Skip to content

Couple the gripper fingers and repair the USD regeneration pipeline - #21

Open
johnnynunez wants to merge 1 commit into
Seeed-Projects:mainfrom
johnnynunez:fix/gripper-coupling
Open

Couple the gripper fingers and repair the USD regeneration pipeline#21
johnnynunez wants to merge 1 commit into
Seeed-Projects:mainfrom
johnnynunez:fix/gripper-coupling

Conversation

@johnnynunez

@johnnynunez johnnynunez commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

The bug

The gripper jaws move on their own whenever the arm moves — they swing as if nothing were holding them. It reproduces identically in PhysX, Newton and MuJoCo, which is the tell: all three read the same authored gains, so this is an asset defect rather than a solver one.

Two independent causes, both fixed here.

1. The fingers were 9,248x softer than the actuator they model

Every arm joint sits at ζ 18–49 (overdamped). The fingers were at K = 100 N/m, ζ = 0.685, f_n = 5.45 Hz — underdamped, so any teleop motion excites them like a pendulum hanging off the wrist.

before after
gravity sag (static) 7.375 mm — 14.75% of the 50 mm stroke ~0 mm
peak drift, arm slewing at 0.5 Hz 2.344 mm 0.006 mm

The new gains are derived from the hardware, not tuned by hand. Motor 7 is a RobStride whose limit_torque reads 14 Nm live from the firmware over CAN; it drives both racks through one pinion of r = 7.353 mm/rad (from the calibrated 0 → −6.8 rad travel over the 50 mm stroke). Reflecting the motor's 50 Nm/rad through that transmission gives K = 50/r² = 925 kN/m and F = 14/r = 1904 N at the finger. 925 kN/m is far above what the solver can integrate at 1/120 s, so the drive is capped at the stiffest gain that stays smooth, with damping set for ζ = 1.0.

Same reasoning for the limits: effort 500 → 1904 N, maxVelocity 10 → 0.243 m/s (10 m/s was ~41x faster than the motor's 33 rad/s can actually drive the fingers).

2. The fingers were modelled as two independent DOFs

The real gripper is one motor, one pinion, two opposed racks (the hardware BOM ships 02_Rack.step ×2), so finger travel is rigidly 1:1. The asset handed the solver a degree of freedom the robot does not have — which is why the jaws could separate at all.

USD now authors PhysxMimicJointAPI on joint_right referencing joint_left; the MJCF authors the equivalent <equality joint>.

Two subtleties worth recording:

  • PhysX constrains q_follower + gearing·q_leader + offset = 0, so gearing = -1 is what makes the two coordinates equal — the finger axes are already antiparallel in the parent frame. Using +1 yields q_right = -q_left, which looks perfectly symmetric while collapsing to the wrong opening. MuJoCo hides this; only running both engines catches it.
  • The MJCF equality uses the standard positive solref form (0.004 1, timeconst = 2·dt). Direct constraint gains (negative solref) couple ~11x tighter in MuJoCo but make Newton's SolverMuJoCo diverge to NaN, and this model is published for both engines.

3. Asymmetric finger limits in the URDF

joint_left allowed 0–0.05 m, joint_right 0–0.0715 m. Both fingers use the same mesh, their travel axes are exactly antiparallel (dot product −1.000000) and their origins sit 0.147 mm apart, so the stroke is symmetric — 0.0715 was an authoring error, and it also made a 1:1 coupling impossible to express.

Pipeline repairs this depends on

prep_asset.py could not run at all on main. #18 renamed the payload layers (base.usdaRS-rebot-dev-arm_base.usd, physics.usdaRS-rebot-dev-arm_physics.usd) without updating the script, so it died in its first function:

AttributeError: 'NoneType' object has no attribute 'GetPrimAtPath'

The five stale paths and the ./instances.usda re-anchor are fixed, so the USD is regenerable again.

Physics=none was not neutral. The physics payload was attached to the root prim as well as inside the variant, so it composed in every selection and the "no physics" variant still carried 10 colliders and 10 rigid bodies — the exact condition prep_asset.py already asserted against, and a SimReady Robot-Body Neutral requirement. Removing the duplicate root-prim payload leaves the variant as the only opinion:

physics  -> {'convexHull': 7, 'convexDecomposition': 3}
mujoco   -> {'convexHull': 7, 'convexDecomposition': 3}
none     -> {}

The gain assertions compared floats with ==, which only held while every authored gain was integral; USD stores them as float32, so 41.28 reads back as 41.279998779296875. They now compare with a tolerance. verify() additionally asserts the mimic survives regeneration, so the coupling cannot be silently lost by a future re-export.

Validation

Isaac Sim 6.1 (PhysX), headless, articulation loaded from the asset:

[isaac] articulation root: /tn__00armrs_asmv3_hJ6D/Geometry/base_link
[isaac] PhysxMimicJointAPI:Z -> reference=joint_left, gearing=-1.0, offset=0.0
  mimic composed : True
  static  : dev 0.00006 mm   asym 0.00011 mm
  slewing : dev 0.00629 mm   asym 0.01204 mm
  VERDICT : PASS

The mimic composes through the payload and variant, not merely exists in the physics layer.

MuJoCo, same experiment: neq=1, dev 0.017 mm, asym 0.034 mm.

validate_physics_fidelity.py: failures: [], 10 links and 8 joints checked across the physics and mujoco variants, with the compiled-MJCF cross-check running (mujoco 3.10.0) rather than skipped.

prep_asset.py: all three variants verify, and re-running is a byte-level no-op.
build_mjcf.py: regenerates rebot_devarm.xml byte-identical to the committed file, so the generator stays the source of truth.

Net effect

Measured within each engineupstream/main's asset and this one through the identical experiment, arm sweeping joint1 ±0.4 rad and joint2 ±0.2 rad:

slew PhysX before PhysX after ratio MuJoCo before MuJoCo after ratio
0.25 Hz 0.283 mm 0.0064 mm 44× 0.886 mm 0.017 mm 52×
0.5 Hz 0.580 mm 0.0128 mm 45× 1.900 mm 0.033 mm 57×
1 Hz 1.338 mm 0.0305 mm 44× 1.792 mm 0.066 mm 27×
2 Hz 3.428 mm 0.0687 mm 50× 2.159 mm 0.147 mm 15×
4 Hz 5.760 mm 0.1353 mm 43×
8 Hz 6.797 mm 0.2526 mm 27×

The fingers are not motionless — deviation grows with slew rate — but the improvement holds at every speed rather than only at gentle ones, and the degradation is gradual with no instability threshold. At 8 Hz, well beyond any teleop rate, the residual 0.25 mm is 0.5% of the 50 mm stroke; the unfixed asset already exceeded 1% of stroke at 0.5 Hz. Static gravity sag goes from 7.375 mm to ~0.

For reference, the physical gripper unpowered reads 268.5 µrad of encoder noise (0.00197 mm at the finger), so the fixed asset sits 3× that floor at 0.25 Hz and 128× at 8 Hz.

Why the jaws move at all

A single motor holds them, so zero motion is a reasonable expectation. Documented in usd/RS-rebot-dev-arm/docs/GRIPPER_RESIDUAL_MOTION.md, with the raw data under evidence/.

It is not drive compliance — raising the gripper stiffness 16× barely moves the number, and the actuator uses 0.88 N of the 1904 N available:

drive stiffness 1 250 2 500 5 000 (shipped) 10 000 20 000 N/m
deviation 0.1516 0.1481 0.1466 0.1460 0.1459 mm

It is not coupling softness either — tightening solref past the shipped 0.004 1 changes nothing (0.1466 mm at 0.004, 0.002 and 0.001).

With the arm held still deviation is 0.00000 mm. It appears only under wrist acceleration — and gets worse when the arm tracks better:

arm gains joint1 tracking error finger deviation
as shipped 266.8 mrad 0.1466 mm
×4 151.8 mrad 0.3664 mm
×16 78.6 mrad 0.7739 mm
frozen 0 0.0000 mm

A stiffer arm follows the sinusoid more closely, so the wrist accelerates harder. The jaws have mass (75.2 g each), and a position drive produces force only from a position error: dx = m·a/K. Ordinary rigid-body mechanics, not a modelling defect.

What the real motor would do

The drive must ship at 5 kN/m rather than the motor's reflected 925 kN/m (50 N·m/rad through a 7.353 mm/rad pinion) because the real value diverges at dt = 1/120 s. At the real stiffness, the same accelerations give:

slew asset (5 kN/m) real drive (925 kN/m)
0.5 Hz 0.0333 mm 0.000112 mm
2 Hz 0.1466 mm 0.000495 mm
4 Hz 0.3485 mm 0.001019 mm

All below the 0.00197 mm encoder noise floor — the physical gripper does not move measurably. The simulated residual is a timestep budget, not an asset parameter: deviation falls with dt (measured at 1/240, 1/480 and 1/960 s), which is the knob for anyone needing sub-0.01 mm fidelity.

Measured on the physical arm

RobStride motor 7 over can0 in MIT mode, after writing and verifying the official MotorBridge Studio parameter template. Full write-up in GRIPPER_RESIDUAL_MOTION.md, raw data under evidence/.

quantity measured at the finger
breakaway torque 0.100 N·m 13.6 N
closed-loop stiffness at kp = 3 N·m/rad 66 759 N/m
backlash ±0.05 rad ±0.37 mm

The stiffness is the median of 12 plateaus spanning 64 800–69 900 N/m (4 % spread) including the reversed-sign return sweep; theory predicts kp/r² = 55 487 N/m, so the measurement is 1.20× that.

Two plausible fixes, both refuted by measurement

Writing the measured friction into the asset makes it worse. Real friction exceeds the inertial load on the jaws by 14–130×, so the physical gripper genuinely cannot be shifted by wrist acceleration. But in simulation:

frictionloss 0.5 Hz 2 Hz 4 Hz
0.2 N (shipped) 0.0333 0.1466 0.3485 mm
6.8 N (measured) 0.0381 0.1832 0.3486 mm

The joint is driven by the coupling constraint, and friction cannot oppose a constraint — it only forces the solver to push harder.

Tightening the constraint does nothing. It is already saturated:

solref 0.008 1 0.004 1 (shipped) 0.002 1 0.001 1 0.0002 1
deviation 0.5817 0.1466 0.1466 0.1466 0.1466 mm

Measuring internal forces shows the constraint applies more force than the actuator (1.207 N vs 0.877 N at 2 Hz) and accounts for 43 % of the residual — disabling it drops 0.1466 → 0.0840 mm. That also corrects an earlier claim in this PR: inertial load via dx = m·a/K is real but secondary; the constraint dominates.

The only real lever is the integration rate

rate 0.5 Hz 2 Hz 8 Hz coupling error
500 Hz (asset native) 0.0332 0.1466 0.5032 0.00000 mm
1000 Hz 0.0163 0.0737 0.3355 0.00000 mm
2000 Hz 0.0082 0.0375 0.1757 0.00000 mm
4000 Hz 0.0043 0.0195 0.0922 0.00000 mm

Clean 1/dt convergence with the 1:1 coupling exact at every rate.

SDK behaviour worth knowing before reproducing this

Three properties of third_party/reBotArm_control_py cost several failed runs and silently corrupt measurements:

  • send_mit() is ignored unless ensure_mode(Mode.MIT) is called first. No error is raised — the frames are accepted, telemetry keeps flowing, and measured torque simply never tracks the command. A ramp from 0.05 to 2.0 N·m produced a flat −0.13 N·m at every step. run_mode is 0x7005, read as i8 (robstride_get_param_f32 raises "parameter type mismatch"): 0 = MIT, 1 = POS_VEL.
  • send_pos_vel(pos, vlim) persists vlim into the motor. It is not carried in the command frame; it writes parameter 0x7017 (limit_spd). mode_pos_vel() likewise writes 0x7017, 0x701E, 0x701F and 0x7020 from the library YAML, overwriting the vendor's calibrated per-joint template without warning. Repair via MotorBridge Studio (Read → Apply Default Template → Write), noting the template is per-joint, not uniform.
  • Position-mode stall detection is not viable at 40 Hz feedback. The drive goes from free to hard stop within one sample interval, so a 1.0 N·m threshold was never observed — consecutive samples read ≈0 then −3.76 N·m. Torque mode bounds the effort by construction instead.

Reproducing every table

script measures needs the arm
measure_gripper_stiffness.py closed-loop stiffness, MIT mode yes
measure_gripper_breakaway.py breakaway torque yes
sweep_gripper_friction.py residual vs frictionloss no
sweep_coupling_constraint.py residual vs solref / solimp no
sweep_timestep_convergence.py residual vs integration rate no

Both hardware scripts take --dry-run, which exercises the full path against live telemetry without enabling the motor. All resolve paths relative to the repo and were verified from a clean working directory.

No asset parameters were changed as a result. The mechanism model is correct — fingers coupled exactly, drive not the limit, hardware friction mechanically irrelevant to what the solver is doing. Only the note and its evidence are added.

README correction

The gripper-limit tables in README.md / README_EN.md / README_ES.md still documented joint_right's 0.0715 m upper limit. They now state the measured symmetric 0.05 m on both fingers, and that one motor drives both through a single pinion.

SimReady conformance

Validated with the official simready-validate from NVIDIA/simready-foundation. Robot-Body-Neutral now PASSES — it failed on upstream/main before this change.

Making Physics=none genuinely neutral had to be reconciled with ISA.001, which requires the _physics.usd payload on the default prim in the root layer (isaac_sim/composition/validation.py reads prim_spec.payloadList directly, so a payload authored inside a variant does not count). A root-prim payload composes under every selection, so the two requirements pull opposite ways.

The variant now subtracts what the payload adds: inside Physics=none the /Physics scope and the collision instances are deactivated, and the body-level physics API schemas are stripped. ISA.001 keeps its payload, and the neutral selection composes to plain geometry:

physics  colliders={'convexHull': 7, 'convexDecomposition': 3}  rigidbodies=10  joints=10
mujoco   colliders={'convexHull': 7, 'convexDecomposition': 3}  rigidbodies=10  joints=10
none     colliders={}                                          rigidbodies= 0  joints= 0

This is authored by prep_asset.py, not by hand: deleting the variant and re-running regenerates it, and re-running again is byte-identical.

Three upstream problems found on the way

Reported in NVIDIA/simready-foundation#18:

  • DJ.001 crashes on any joint carrying PhysxMimicJointAPI. validation.py lines 258 and 535 call .Get() on a float that lines 256–257 already resolved. Only reachable with a coupled mechanism, which is why it had gone unnoticed. With a one-line fix applied locally, Robot-Body-Neutral goes from FAILED to PASSED.
  • DJ.007 only accepts rotational mimic axes (rotX/rotY/rotZ), so no linear coupled gripper can satisfy it — even though UsdPhysics defines transX/Y/Z and PhysX's own Newton migration checker documents prismatic mimic support.
  • DJ.004 wants zero drive gains on a mimic follower. Correct for a revolute pair, but measured in Isaac Sim, PhysX does not enforce PhysxMimicJointAPI on a prismatic joint at all:
case mimic slew deviation asymmetry
mimic + follower drive True 0.0064 mm 0.0123 mm
no mimic + follower drive False 0.0064 mm 0.0123 mm
mimic + zero gains (DJ.004) True 51.5009 mm 51.4996 mm
no mimic + zero gains False 51.5009 mm 51.4996 mm

Identical to four decimal places with and without the constraint. Complying with DJ.004 would leave the gripper worse than the original bug (51.5 mm vs 2.3 mm), so the follower keeps its drive: Newton and MuJoCo honour the constraint, PhysX is carried by the drive. The same probe on a revolute pair does show the constraint acting.

The PhysicsScene is no longer authored by the asset

RC.005 rejects physics attributes authored in the interface layer, and SimReady's own reference robot (sample_content/.../Robotiq/2F-85/simready_isaac_usd) ships without one. Three placements were measured; none satisfies every constraint at once:

scene authored at composes in physics layer leaks on reference
interface layer /PhysicsScene yes no (fails RC.005) no
physics layer /PhysicsScene no
physics layer <robot>/PhysicsScene yes yes yes

A payload arc maps only the defaultPrim subtree, so a scene at the physics-layer root never composes; placed under the robot prim it composes but then travels into any stage that references the robot, which verify() forbids.

Gravity belongs to the stage that loads the robot — Isaac Sim, Newton and MuJoCo all supply it. verify() and validate_physics_fidelity.py now assert the asset does not author a scene, so a future re-export cannot reintroduce a hardcoded gravity that fights the host.

Three environment problems that made earlier SimReady results unreliable

Worth knowing before re-running the validator — none of them are asset defects:

  • The stock validate venv installs usd-exchange, whose pxr does not ship PhysxSchema. Every rule touching it reports "PhysxSchema is not available in this environment" as a FAILURE rather than skipping — 161 such messages here. DJ.002 and DJ.003 were false negatives.
  • RC.007 and DJ.008/009/010 import usd.schema.isaac, which ships with the Isaac Sim build rather than the validator. Without it they abort with No module named 'usd.schema.isaac' before inspecting the asset. All four pass once it is on the path.
  • RC.001 and RC.004 need omni.client; without it they raise module 'omni' has no attribute 'client'. Both still report that here, so their status is unknown rather than failing: the asset root holds only the interface layer (what RC.001 wants) and .thumbs/256x256/RS-rebot-dev-arm.usda.png exists (what RC.004 wants).

Any prior "all profiles PASS" claim for this asset should be re-measured on that basis.

Where the profiles stand

profile status
Robot-Body-Neutral PASSED (was FAILED on main)
Robot-Body-Runnable DJ.004 / DJ.006 / DJ.007 — all three are the PhysX prismatic-mimic issue above
Robot-Body-Isaac the same three, plus RC.001 / RC.004 blocked on omni.client

ISA.001, RC.005, RC.007, BA.002, DJ.001, DJ.002, DJ.003, DJ.008, DJ.009 and DJ.010 now pass — every one of them failed on upstream/main.


Supersedes #13, which was branched before #14, #17 and #18 landed and had grown conflicts in 9 files.

@johnnynunez
johnnynunez force-pushed the fix/gripper-coupling branch 5 times, most recently from 0a376af to 7de1aee Compare August 2, 2026 23:08
The gripper jaws drift on their own whenever the arm moves. This is an asset
defect, not a solver one: it reproduces identically in PhysX, Newton and MuJoCo
because all three read the same authored gains.

Two independent causes, both fixed here.

1. The fingers were 9,248x softer than the actuator they model.
   Every arm joint sits at zeta 18-49 (overdamped) while the fingers were at
   K = 100 N/m, zeta = 0.685, f_n = 5.45 Hz -- underdamped, so any teleop motion
   excites them. They sagged 7.4 mm under gravity alone (14.75% of the 50 mm
   stroke) and peaked 2.3 mm during an arm sweep.

   The new gains come from the hardware, not from tuning. Motor 7 is a RobStride
   whose limit_torque reads 14 Nm live from the firmware over CAN; it drives both
   racks through one pinion of r = 7.353 mm/rad (derived from the calibrated
   0 -> -6.8 rad travel over the 50 mm stroke). Reflecting the motor's 50 Nm/rad
   through that transmission gives K = 50/r^2 = 925 kN/m and F = 14/r = 1904 N at
   the finger. 925 kN/m is far above what the solver can integrate at 1/120 s, so
   the drive is capped at the stiffest gain that stays smooth, with damping set
   for zeta = 1.0.

   Same reasoning for the limits: effort 500 -> 1904 N, and maxVelocity
   10 -> 0.243 m/s (10 m/s was ~41x faster than the motor's 33 rad/s can drive
   the fingers).

2. The fingers were modelled as two independent DOFs.
   The real gripper is one motor, one pinion and two opposed racks (hardware BOM
   ships 02_Rack.step x2), so finger travel is rigidly 1:1. The asset gave the
   solver a degree of freedom the robot does not have, which is why the jaws
   could separate at all.

   USD now authors PhysxMimicJointAPI on joint_right referencing joint_left, and
   the MJCF authors the equivalent <equality joint>. Note PhysX constrains
   q_follower + gearing * q_leader + offset = 0, so gearing = -1 is what makes the
   two coordinates EQUAL -- the finger axes are already antiparallel in the parent
   frame. Using +1 yields q_right = -q_left, which looks perfectly symmetric while
   collapsing to the wrong opening; MuJoCo hides this and only running both
   engines catches it.

   The MJCF equality uses the standard positive solref form (0.004 1,
   timeconst = 2*dt). Direct constraint gains (negative solref) couple ~11x
   tighter in MuJoCo but make Newton's SolverMuJoCo diverge to NaN, and this
   model is published for both engines.

The URDF's asymmetric finger limits are also corrected: joint_left allowed
0-0.05 m and joint_right 0-0.0715 m. Both fingers use the same mesh, their travel
axes are exactly antiparallel (dot product -1.000000) and their origins sit
0.147 mm apart, so the stroke is symmetric and 0.0715 was an authoring error --
it also made a 1:1 coupling impossible to express.

Pipeline repairs this depends on
--------------------------------

prep_asset.py could not run at all on main. Seeed-Projects#18 renamed the payload layers
(base.usda -> RS-rebot-dev-arm_base.usd, physics.usda ->
RS-rebot-dev-arm_physics.usd) without updating the script, so it died in its
first function with AttributeError: 'NoneType' object has no attribute
'GetPrimAtPath'. The five stale paths and the ./instances.usda re-anchor are
fixed, so the USD is regenerable again.

Physics=none was not neutral. The physics payload was attached to the root prim
as well as inside the variant, so it composed in every selection and the "no
physics" variant still carried 10 colliders and 10 rigid bodies -- the exact
condition prep_asset.py already asserted against, and a SimReady Robot-Body
Neutral requirement. Removing the duplicate root-prim payload leaves the variant
as the only opinion. Physics=none now composes zero colliders and zero rigid
bodies, while physics and mujoco keep their 10 (7 convexHull, 3
convexDecomposition).

The gain assertions compared floats with ==, which only held while every authored
gain was integral; USD stores them as float32, so 41.28 reads back as
41.279998779296875. They now compare with a tolerance. verify() additionally
asserts the mimic survives regeneration, so the coupling cannot be silently lost.

Validation
----------

Isaac Sim 6.1 (PhysX), headless, articulation loaded from the asset:

    PhysxMimicJointAPI:Z composed, reference=joint_left, gearing=-1.0
    static  : finger dev 0.00006 mm   asym 0.00011 mm
    slewing : finger dev 0.00629 mm   asym 0.01204 mm

MuJoCo, same experiment: dev 0.017 mm, asym 0.034 mm, neq=1.

validate_physics_fidelity.py: failures: [], 10 links and 8 joints checked across
the physics and mujoco variants, with the compiled-MJCF cross-check running
(mujoco 3.10.0) rather than skipped.

prep_asset.py: all three variants verify and re-running is a byte-level no-op.
build_mjcf.py regenerates rebot_devarm.xml byte-identical to the committed file.

Improvement, measured within each engine on the same experiment (upstream/main's
asset vs this one), arm sweeping joint1 +/-0.4 rad and joint2 +/-0.2 rad:

    Isaac Sim 6.1 / PhysX          MuJoCo 3.10
    slew    before    after         slew    before    after
    0.25Hz  0.283mm   0.0064mm      0.25Hz  0.886mm   0.017mm
    0.50Hz  0.580mm   0.0128mm      0.50Hz  1.900mm   0.033mm
    1.00Hz  1.338mm   0.0305mm      1.00Hz  1.792mm   0.066mm
    2.00Hz  3.428mm   0.0687mm      2.00Hz  2.159mm   0.147mm
    4.00Hz  5.760mm   0.1353mm
    8.00Hz  6.797mm   0.2526mm

Roughly 44x across the range in PhysX and 15-57x in MuJoCo. The fingers are not
motionless -- deviation grows with slew rate -- but the improvement holds at
every speed rather than only at gentle ones, and the degradation is gradual
with no instability threshold. At 8 Hz, well beyond any teleop rate, the
residual 0.25 mm is 0.5% of the 50 mm stroke; the unfixed asset exceeded 1% of
stroke by 0.5 Hz. Static gravity sag goes from 7.375 mm to ~0.

For reference the physical gripper, unpowered, reads 268.5 urad of encoder noise
(0.00197 mm at the finger), so the fixed asset sits 3x that floor at 0.25 Hz and
128x at 8 Hz.

Why the jaws move at all is documented in
usd/RS-rebot-dev-arm/docs/GRIPPER_RESIDUAL_MOTION.md, since a single motor holding
them is a reasonable reason to expect zero. In short: it is not drive compliance
(16x stiffer changes 0.1516 -> 0.1459 mm, and the actuator uses 0.88 N of 1904 N),
and it is not coupling softness (tightening solref past the shipped 0.004 changes
nothing). With the arm held still the deviation is 0.00000 mm; it appears only
under wrist acceleration, and gets worse when the ARM tracks better (0.147 mm at
shipped gains, 0.774 mm at 16x) because a stiffer arm accelerates the wrist
harder. The jaws have mass, so dx = m*a/K -- ordinary rigid-body mechanics.

The drive must ship at 5 kN/m rather than the motor's reflected 925 kN/m
(50 N*m/rad over a 7.353 mm/rad pinion) because the real value diverges at
dt = 1/120 s. At the real stiffness the same accelerations give 0.0001-0.0010 mm,
below the 0.00197 mm encoder noise floor -- so the physical gripper does not move
measurably, and the simulated residual is a timestep budget, not an asset
parameter. Deviation falls with dt (1/240 s, 1/480 s, 1/960 s measured), which is
the knob for anyone needing sub-0.01 mm fidelity.

The README gripper-limit tables are corrected alongside: they still documented
joint_right's 0.0715 m upper limit, which this change replaces with the measured
symmetric 0.05 m on both fingers.

Follow-up measurements on the physical arm (RobStride motor 7 over can0, MIT
mode, after writing and verifying the official MotorBridge Studio parameter
template) refine that note further, and refute two plausible fixes:

  breakaway torque    0.100 N*m  -> 13.6 N at the finger
  closed-loop K       66,759 N/m at kp=3 N*m/rad (median of 12 plateaus,
                      64,800-69,900, 4% spread; theory kp/r^2 = 55,487, so 1.20x)
  backlash            +/-0.37 mm, both directions free from a closed stop

Real friction exceeds the inertial load on the jaws by 14-130x, so the physical
gripper cannot be shifted by wrist acceleration at all. Writing that friction
into the asset was tested and makes the simulation WORSE (2 Hz: 0.1466 ->
0.1832 mm), because in sim the joint is driven by the coupling constraint, and
friction cannot oppose a constraint -- it only makes the solver push harder.

Measuring the internal forces shows the constraint applies more force than the
actuator (1.207 N vs 0.877 N at 2 Hz) and accounts for 43% of the residual
(disabling it: 0.1466 -> 0.0840 mm). It is already saturated: solref from
0.004 down to 0.0002 gives an identical 0.1466 mm. Loosening solimp lowers the
number only by weakening the coupling, which is not a fix.

That leaves the integration rate, which converges cleanly as 1/dt from the
asset's native 500 Hz (0.1466 mm at 2 Hz) to 4000 Hz (0.0195 mm), with the 1:1
coupling exact (|left-right| = 0.00000 mm) at every rate.

Conclusion: no asset parameter changes. The mechanism model is correct, and the
residual is a documented solver floor rather than a defect. Only the note, its
evidence and the measurement scripts are added.

Three properties of third_party/reBotArm_control_py are documented alongside,
since they cost several failed runs and silently corrupt measurements:

  * send_mit() is ignored unless ensure_mode(Mode.MIT) is called first. No
    error is raised; measured torque simply never tracks the command (a 0.05 ->
    2.0 N*m ramp produced a flat -0.13 N*m). run_mode is 0x7005, read as i8.
  * send_pos_vel(pos, vlim) does not carry vlim in the command frame -- it
    writes parameter 0x7017 (limit_spd). mode_pos_vel() likewise writes 0x7017,
    0x701E, 0x701F and 0x7020 from the library YAML, overwriting the vendor's
    calibrated per-joint template without warning.
  * position-mode stall detection is not viable at the 40 Hz feedback rate: the
    drive goes from free to hard stop within one sample, so a 1.0 N*m threshold
    was never observed (consecutive samples read ~0 then -3.76 N*m).

Five scripts reproduce every table: measure_gripper_stiffness.py and
measure_gripper_breakaway.py need the arm (both take --dry-run, which exercises
the full path against live telemetry without enabling the motor);
sweep_gripper_friction.py, sweep_coupling_constraint.py and
sweep_timestep_convergence.py are simulation-only. All resolve their paths
relative to the repo and were verified from a clean working directory.

SimReady conformance
--------------------

Validated with the official simready-validate from NVIDIA/simready-foundation.
Robot-Body-Neutral now PASSES; it failed on upstream/main before this change.

Making Physics=none genuinely neutral had to be reconciled with SimReady
ISA.001, which requires the _physics.usd payload on the default prim in the
ROOT layer (isaac_sim/composition/validation.py reads prim_spec.payloadList
directly, so a payload authored inside a variant does not count). A root-prim
payload composes under every selection, so the two requirements pull opposite
ways. The variant now subtracts what the payload adds: inside Physics=none the
/Physics scope and the collision instances are deactivated and the body-level
physics API schemas are stripped. ISA.001 keeps its payload and the neutral
selection composes to plain geometry.

Three upstream problems were found while doing this and are reported in
NVIDIA/simready-foundation#18:

- DJ.001 crashes on any joint carrying PhysxMimicJointAPI: validation.py lines
  258 and 535 call .Get() on a float that lines 256-257 already resolved. Only
  reachable with a coupled mechanism, which is why it had gone unnoticed.
- DJ.007 only accepts rotational mimic axes (rotX/rotY/rotZ), so no linear
  coupled gripper can satisfy it, even though UsdPhysics defines transX/Y/Z and
  PhysX's own Newton migration checker documents prismatic mimic support.
- DJ.004 wants zero drive gains on a mimic follower. That is correct for a
  revolute pair, but measurements in Isaac Sim show PhysX does not enforce
  PhysxMimicJointAPI on a PRISMATIC joint at all -- removing the API entirely
  gives numbers identical to four decimal places, and with zero gains the
  fingers drift 51.5 mm, far worse than the 2.3 mm bug this fixes. The follower
  therefore keeps its drive: Newton and MuJoCo honour the constraint, PhysX is
  carried by the drive.

The PhysicsScene is no longer authored by the asset. RC.005 rejects physics
attributes authored in the interface layer, and SimReady's own reference robot
(sample_content/.../Robotiq/2F-85) ships without one. Three placements were
measured and none satisfies every constraint at once:

    interface layer /PhysicsScene         composes, fails RC.005
    physics layer   /PhysicsScene         never composes (a payload arc maps
                                          only the defaultPrim subtree)
    physics layer   <robot>/PhysicsScene  composes and passes RC.005, but leaks
                                          into any stage referencing the robot

Gravity belongs to the stage that loads the robot: Isaac Sim, Newton and MuJoCo
all supply it. verify() and validate_physics_fidelity.py now assert the asset
does NOT author a scene, so a future re-export cannot reintroduce a hardcoded
gravity that fights the host.

Three environment problems made earlier SimReady results unreliable, and are
worth knowing before re-running the validator:

- The stock validate venv installs usd-exchange, whose pxr does not ship
  PhysxSchema. Every rule touching it reports "PhysxSchema is not available in
  this environment" as a FAILURE rather than skipping -- 161 such messages here.
  DJ.002 and DJ.003 were false negatives.
- RC.007 and DJ.008/009/010 import usd.schema.isaac, which ships with the Isaac
  Sim build rather than the validator. Without it they abort with "No module
  named 'usd.schema.isaac'" before inspecting the asset. All four pass once it
  is on the path.
- RC.001 and RC.004 need omni.client; without it they raise "module 'omni' has
  no attribute 'client'". Both still report that here, so their status is
  unknown rather than failing: the asset root holds only the interface layer
  (what RC.001 wants) and .thumbs/256x256/RS-rebot-dev-arm.usda.png exists
  (what RC.004 wants).
@johnnynunez
johnnynunez force-pushed the fix/gripper-coupling branch from 7de1aee to 6a18bea Compare August 2, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant