Tianji differential IK¶
UMI/Tianji now selects policy.adapter.ik_backend: analytic | diff. The default
remains analytic: its SDK solution, FK tolerances, 1.8-degree branch check,
joint-limit margins and J6/J7 check are unchanged. The optional diff backend
is implemented in manimux/kinematics/tianji_diff.py, using the existing
TianjiKinematics DH chain, flange FK, tool transform and joint limits.
The port follows SII-LiuLab/tianji-control revision
1e7dfdbc94c62f87501d6485b8c0e43ce6dbf513, specifically algos/diff_ik.py,
algos/kinematics.py and JointLimitAvoidance in algos/nullspace.py.
The position-lag guard follows CalibWrist deploy/tianji/ik_guard.py and
real_run.py at f17a62a2ab77207de54fdca73dc8ff4d76f87cdb.
Production code imports neither checkout. OSQP, NumPy and SciPy are sufficient;
this backend loads no torch, robot connection or vendor IK library.
Configuration¶
Install the runtime extra (the model environment does not need it):
Use the same checkpoint binder as the analytic path:
envs/umi_dp/.venv/bin/python manimux/servers/umi_dp.py \
--experiment manimux/configs/experiments/pass_ball/umi_dp/tianji_taccap_umi_dp_diff.yaml \
--checkpoint /path/to/trusted/pass_ball.ckpt \
--bind-runtime-config data/experiments/pass-ball-diff.yaml
The checked-in differential-IK experiment references the shared embodiment profile:
Use the same reference from another Tianji experiment. The model, checkpoint identity and RTC sampler stay the same. IK selection is an embodiment setting. The config loader expands the profile, and the checkpoint binder writes these tuning options:
policy:
adapter:
ik_backend: diff
ik_validation_dt_s: 0.004
diff_ik:
w_pos: 1.0
w_rot: 1.0
lam: 0.001
limit_margin_deg: null
j67_margin_deg: null
mu_nullspace: 1000.0
nullspace_activation_deg: 25.0
nullspace_weights: null
max_lag_mm: 5.0
max_lag_deg: null
lag_policy: report
The serialized adapter config contains solver tuning only. At runtime the adapter
receives executor.motion_limits directly and gives the differential solver the
resolved motion_limits.arm.max_velocity. This keeps one copy of the arm speed
limit; the checkpoint binder and history strategy no longer copy or compare it.
A finite executor max_step_dt_s is still required by the Tianji control profile,
but it remains an executor command-budget setting rather than a solver field.
The QP uses independent per-joint velocity constraints regardless of the shared
executor's per_joint/isotropic mode; this matches the reference differential
solver. Executor shaping and the command guard still apply afterwards.
They may further alter the predicted joint trajectory.
Null joint-margin settings inherit this arm's configured kinematic margin
(5 degrees by default); the diff solver cannot lower that margin. The right
arm's J6 override remains ±58 degrees before the margin. The J6/J7 interference
constraint is mandatory and is not configurable. Positive j67_margin_deg can
override the QP buffer.
Optional nullspace_weights must contain seven finite nonnegative weights;
nullspace_activation_deg: null selects the original global quadratic cost.
Weights, regularization, activation, lag and margin settings are strictly validated.
Algorithm and failure behavior¶
Each step solves the original velocity QP:
minimize ||J qdot - desired_twist||²_W + lam ||qdot||²
+ mu * dt * gradient(joint_limit_cost) · qdot
subject to joint-velocity and margin-adjusted position boxes,
the linearized J6/J7 interference inequality.
External poses/joints use metres/radians. Internally the QP retains mm/degrees to preserve the reference weighting and regularization. Its spatial orientation error is a rotation vector, not an Euler-coordinate delta. The flange Jacobian uses modified DH's post-link joint axes and includes the static flange offset. Targets are transformed from TCP to flange using the same mounted tool as FK.
The solver uses the duration supplied by its caller and clips numerical OSQP
overshoot to the velocity box. It rejects empty boxes before OSQP.update so OSQP cannot silently
reuse a stale problem. Unsolved statuses and nonfinite solutions fail; solved
steps are checked again for position margin and J6/J7 interference. Fixed sparse
patterns and within-chunk primal warm starts preserve the upstream QP procedure.
Differential IK is a rate controller: an unreachable target can produce a valid
small joint step with a large residual. The mandatory positive max_lag_mm
therefore defaults to CalibWrist's 5 mm guard. This residual is measured at the
flange, after the step; it is not a live TCP tracking monitor. Optional
max_lag_deg uses the reference maximum wrapped XYZ Euler-coordinate residual,
not a geodesic rotation angle. It defaults to null, as in CalibWrist.
lag_policy decides what a residual over these thresholds means. abort (the
library default) turns it into a tracking_lag failure. report, selected by
the pass-ball experiment and the reusable Tianji override, keeps the bounded
step and only counts it; the adapter records each arm's worst residual
and exceedance count in the chunk metadata as diff_ik_lag. Failed or empty QPs,
nonfinite solutions, joint margins and J6/J7 interference reject under both
policies, and any such failure rejects the entire predicted chunk.
Each arm resets both OSQP primal and dual state at the beginning of each speculative chunk. The prior measured state seeds the new solve, so rejected plans or a prior engagement cannot carry QP state into a new chunk. The original CalibWrist reset only zeroed its previous velocity; this intentional difference makes the new chunk independent of that hidden solver history.
Timing and validation¶
IK runs while decoding a predicted chunk, with SE(3) segments divided into
at-most-4ms steps by default. The optional
ik_validation_dt_s adapter override changes that internal subdivision. Each source knot uses
the fixed policy action interval, and each QP substep gets its actual subdivision duration.
The adapter retains every decoded source knot. At commit time, the shared timeline removes
expired rows and starts from the first row at or after the execution boundary. Only final
joint knots are retained. The shared executor then interpolates those joint knots and sends
commands on control ticks. CalibWrist samples TCP at the
control rate, solves each sample seeded from the preceding command, and retains
all those dense joint commands; it can precompute an entire chunk, and its async
path has a separate sender thread. The difference is which samples are retained
and how sending is scheduled, not a requirement to solve IK live on every tick.
Residual checks on predicted substeps do not establish executed Cartesian path
equivalence.
Reproduce offline checks without any devices:
.venv/bin/python -m pytest tests/unit/test_tianji_diff_ik.py tests/unit/test_umi_dp_tianji.py
.venv/bin/python scripts/validation/tianji_diff_ik_parity.py \
--reference /path/to/tianji-control --output /tmp/tianji-diff-parity.json
.venv/bin/python scripts/validation/umi_dp_tianji_decode_probe.py \
--ik-backend diff --output /tmp/umi-dp-diff-decode.json
On the integration machine, OSQP 1.1.3, the real reference modules were compared
over 1,200 steps: both arms, five poses including all J6/J7 quadrants and active
nullspace bands, and dt values 0.5/4/8/30 ms. All verdicts agreed. Maximum joint
difference was 3.93e-5 degrees; maximum position-residual difference was
1.47e-6 mm. Metre/mm roundoff can perturb OSQP's retained dual state, so this is
a numerical comparison, not bitwise equality; the probe uses a 1e-4 degree
tolerance. Source SHA256 hashes and exact dependency version are in its report.
Single-step solver medians were 0.22–0.26 ms. Full two-arm inline decoding
took 65–66 ms for H16 and 260–262 ms for H64, on small reachable synthetic poses.
On 2026-09-14 the Python around the QP was reduced without changing the step:
closed-form rotation vector/Euler/Rodrigues instead of per-step scipy Rotation,
the rigid-transform check written out with np.allclose's own tolerances, and the
post-step flange Jacobian reused as the next step's start. A chained step dropped
from ~205 us to ~95 us (OSQP itself ~7–8 us); decoding took ~28 ms for H16 and
~112 ms for H64. Against the previous code, verdicts were unchanged and joint
differences stayed below 3e-13 rad over 7,684 recorded steps and 12 chunk decodes.
On 2026-09-21 the same treatment was applied to the composed layer, where the
assembled TCP kinematics had reintroduced per-substep work around the unchanged
solver: kinematics.base.rigid_transform now writes out the allclose/isclose
route it has always used (24 us to 3 us for identical verdicts over 7,909 cases,
including reflections and perturbations at each tolerance), ik validates the
target once instead of again inside flange_target, and the inverse mount/tool
offset is kept for one tool state. IK remains arm-only; the tool contributes the
constant TCP-to-flange change of frame, not a solve. A substep dropped from
~190 us to ~127 us and a two-arm H16 decode from ~55 ms to ~37 ms. Replaying six
frozen chunks (16 knots x 9 substeps, both arms) reproduced the previous joint
trajectories bit for bit, and the full unit suite's failure set was unchanged.
This aggregate chunk work must not block the control thread. The Tianji UMI
profile therefore decodes the complete chunk in two isolated per-arm processes,
then merges both results atomically. Both processes use the request observation
state as their IK seed.
The adapter converts every source knot with the fixed policy action interval;
the shared timeline alone removes rows that have expired at the actual commit
time, selecting the first source row at or after execution starts. Hardware-free
process/RTC tests verify that the control loop continues ticking during decode
and that either arm's failure rejects the whole chunk.
Before this main-layout adaptation, the refactor branch recorded twenty-three differential/adapter tests plus twelve existing UMI tests passing, including finite-difference Jacobians, velocity and dt caps, invalid inputs, empty boxes, J6/J7 conflicts/post-checks, lag guards, reset, profile conflicts, real H16/H64 chunk decoding and atomic rejection on the final right-arm action. Its final combined viewer/session/config/executor/Tianji/camera/UMI/diff-IK and mock-runtime regression suite passed 218 tests in 13.02 seconds. Ruff passed on the changed source, scripts and tests. A real H16 checkpoint was bound with an explicit differential-IK profile in the model environment; the runtime environment loaded the paired config, validated the shared profile, and constructed both QP solvers with no torch or SDK loaded. No hardware motion, real closed-loop policy rollout or task success was tested.