newton.ik · dylanturpin/trajectory-ik

Trajectory IK in Newton

newton.ik.IKSolverTrajectory solves every frame of a joint trajectory jointly — one nonlinear least-squares problem over all frames — instead of frame by frame. Existing per-frame objectives work unchanged; new temporal objectives couple consecutive frames and make the system block-banded, which is what the GPU solve exploits.

python -m newton.examples ik_trajectory · branch dylanturpin/trajectory-ik · numbers on RTX PRO 6000 Blackwell, July 2026

90-frame pick-and-place trajectory re-optimized jointly every viewer frame while the pick waypoint is dragged. Warm-started, CUDA-graph captured.

What was added

Why it's fast: the problem is block-banded

Per-frame costs touch one frame; smoothness costs touch k+1 consecutive frames. So the Gauss–Newton Hessian is block-banded — tridiagonal for velocity costs, wider for acceleration/jerk — and the Newton step costs O(T), not O(T³).

path 1 · direct

Batched block-tridiagonal Cholesky

  • Block Thomas: one CUDA block per trajectory, sequential over frames inside a single wp.tile_cholesky kernel — exact solve.
  • Higher-order stencils reblocked into k·n superblocks, so accel/jerk costs reuse the same tridiagonal kernel.
  • Sequential-in-T cost hidden by batch parallelism: 64 trajectories cost about the same as one.
path 2 · cg

Block-Jacobi preconditioned CG

  • Block-banded Hessian in BSR form (warp.sparse, fixed topology, values rewritten in place) + batched warp.optim.linear.cg.
  • Hand-rolled block-Jacobi preconditioner (per-frame n×n blocks inverted with tile_cholesky_solve), warm-started across LM iterations.
  • Parallel over frames — wins for long horizons; inexact but agrees with direct to ~1e-6.
path 3 · spike (new)

SPIKE: parallel-in-time direct

  • Partitions the horizon; interiors factorize in parallel (one CUDA block per partition per trajectory) with the spike columns as extra right-hand sides.
  • Symmetric Schur complement on the interface blocks — stays SPD, so the reduced system reuses the same Cholesky kernel. Exact: digit-identical LM traces to direct.
  • Sequential depth O(T/P) instead of O(T): 5.8× faster than direct at T=960, 12.6× at T=3840.
paper

ICRA-style writeup

  • Full paper draft (PDF) — formulation, the three backends, cross-library benchmarks with an explicit timing taxonomy, and the G1 retargeting case study.

Results — trajectory solve

Shared task: Franka FR3, T=120 frames @ 30 fps, 4 trajectories, end-effector path through pick-and-place waypoints, 16 LM iterations, analytic Jacobians. The frame-by-frame baseline is the per-frame IKSolver, warm-started and batched across the 4 trajectories.

The full timing grid: batch × horizon, both robots

Filling in the coverage between the spot benchmarks above: steady-state whole-trajectory solve time over T ∈ {120…3840} × B ∈ {1…256} for every applicable backend, at equal 16-iteration budgets on identical FK-feasible targets. On the arm, SPIKE is the fastest backend everywhere beyond the smallest problems (direct edges it by <10% at T=120 and at B=256 for T≤960, where batching already saturates the GPU) — 12.6× over the sequential direct sweep and 233× over the warm-started per-frame loop at (T=3840, B=1); an equal-iteration PyRoKi/jaxls run of the identical problem trails even the per-frame loop (239× vs SPIKE at T=1920; the 12× headline elsewhere on this page is the equal-quality protocol, which lets jaxls terminate early). On the humanoid, CG leads at small batches (88× over per-frame at T=960 B=1, 43× over direct at T=3840) while direct becomes competitive at large batches (1.2× faster than CG at T=240, B=256) once batch parallelism hides its serial depth. The per-frame loop is flat in B — batching rescues most of its throughput gap (to within 1.9× of CG at B=64) but not its greedy quality. SPIKE sits out the G1 rows (the m=35 shared-memory cap); PyRoKi sits out G1 (no off-the-shelf floating-base variable in jaxls, same reason the single-frame comparisons used fixed-base H1).

timing grid figure
(a) FR3 vs horizon at batch 1 (PyRoKi dashed); (b) FR3 vs batch at T=960; (c) G1 vs horizon at batch 1; (d) G1 vs batch at T=960. Median of 5 graph replays.

Humanoid retargeting (G1, soma-retargeter data)

soma-retargeter today drives newton.ik frame by frame to retarget human motion (it produced the G1 motions in BONES-SEED). Same front end here — SOMA BVH clips scaled to 14 world-frame effector targets — but the whole clip is solved jointly on a floating-base 29-DoF G1 (free joint, so the lever-arm coupling matters).

Dance clip retargeted to G1 from the same 14 effector targets. The robot with the orange target markers is the joint trajectory solve; the other replays the raw frame-by-frame loop (soma's production approach, without its post-processing) — watch its arms and knees twitch.

Torque-aware IK: minimize the gravity load, not just the path

Kinematic smoothness says nothing about effort. Two additions couple the trajectory solve to the robot's dynamics. The first, IKObjectiveGravityTorque, penalizes the static holding torque g(q) = ∂U/∂q at every frame — a stencil-0 temporal objective whose dense coefficient block is the analytic gravity Hessian, assembled from subtree mass aggregates and the batched world motion subspace (unactuated floating-base DoFs contribute zero rows). On a Franka carrying 3 kg along a fixed end-effector path, it visibly re-postures the arm and takes the binding shoulder joint from 83% to 61% of its torque limit for a ~25 mm tracking concession; post-hoc torques are verified with Newton's eval_inverse_dynamics (M, Coriolis and gravity terms, central-difference q̇, q̈).

Same 3 kg payload, same end-effector path: posture-reference IK (left) vs the gravity-torque objective (right), with the live shoulder-pitch torque below. The torque-min arm chooses a flatter, gravity-friendly posture throughout.

Honest caveat: manipulator torque limits are generous (a stock FR3 does this task at 83% of its shoulder limit with smoothing alone), so "avoid the limit" only bites with heavy payloads or de-rated drives. The better framing is effort/heat: mean |τ| drops 5–19% here (mechanical energy stays roughly flat — the payload's potential-energy profile is fixed by the path), and the objective composes with everything else in the solve.

Balance-aware IK: the waiter problem, validated in simulation

The second addition, IKObjectiveApparentGravity, is a task-space temporal objective (stencil 2): it penalizes the tangential components of the apparent specific force f = aee − g felt by a surface carried on a link, with aee the second difference of the carried point and the tangent axes evaluated mid-stencil. Its Jacobian combines the acceleration stencil projected on the plate tangents with a plate-tilt term — so the solver both smooths the carried point's acceleration and banks the plate into the motion, like a waiter with a loaded tray. The banded assembly already supports the dense task-space blocks; the objective just pads its two residual rows to the temporal block size.

Validation runs the loop the objective only approximates: the IK output drives a PD-controlled Franka in a MuJoCo forward simulation carrying a plate with a free ball on it (elliptic friction cone, no cheats). With a level-plate orientation objective the dash flings the ball off; with the apparent-gravity objective the plate banks and the ball stays.

Fast 0.4 m dash with a reversal. Left: level-plate IK — the ball rolls off at the first acceleration peak. Right: apparent-gravity IK banks the plate; the ball wanders but stays. Strip chart: ball offset from the plate center in the simulation.

The batch-one extreme: million-frame toolpaths with SPIKE

Corpus retargeting saturates the GPU with batch parallelism, so the horizon-parallel SPIKE backend adds nothing there. Its home turf is the opposite regime: one very long trajectory, fully known before execution — prescribed manufacturing paths. Laser marking and texturing, robotic additive manufacturing, spray coating, and ultrasonic scan coverage all fit: the whole program exists in advance, horizons reach millions of frames, and the arm is low-DoF, which keeps SPIKE's superblocks inside the tile shared-memory budget (the same cap that rules it out on the 35-DoF G1).

The demo engraves a raster image onto a domed panel with an FR3: every pixel sample is one frame of a serpentine toolpath, and the tool is axisymmetric, so a new IKObjectiveAxisAlignment objective points the flange axis along the surface normal while leaving the spin free as redundancy. A 1152×912 raster is a T = 1,050,624-frame trajectory — 2.4 hours of execution at 120 Hz — solved as one nonlinear problem (32 LM iterations, ~9 GiB):

solverwhole-program solvemean trackingmean jerk
joint solve — SPIKE (P=1094)1.1 s0.01 mm1
joint solve — CG2.9 s0.01 mm2
PyRoKi / jaxls, same formulation & budget81.3 s (+ 91 s XLA compile)0.01 mm1
joint solve — sequential direct103.7 s0.01 mm1
frame-by-frame (16 iters/frame, warm-started, graph-captured)701 s0.00 mm5

Everyone tracks at hundredths of a millimeter — the differences are dynamics and latency. A jaxls/PyRoKi formulation of the identical problem (custom axis cost, same weights and iteration budget) lands at 73× the SPIKE time — and to its credit, it does complete the million-frame solve. The greedy per-frame loop carries a 5× acceleration spike into every serpentine reversal, and more damagingly it converts the image content itself into joint-jerk ripple: the engraved figure is literally visible in its vibration-excitation map, exactly where marking quality matters. The joint solve pre-shapes the reversals and confines excitation to the off-image turnaround margins. And the whole-program latency is the workflow win — re-pose the workpiece, re-solve the entire 2.4-hour program in about a second, and check reachability and clearances interactively instead of as a batch job.

116,736-pose engraving program (the mid-size raster), solved jointly in 0.23 s and played back at 400×. The etch accumulates as the tool passes; the whole 2.4-hour million-frame version solves in 1.1 s.
engrave comparison figure
(a) Whole-program solve time vs horizon at batch = 1 — at a million frames the banded solves are 6.8× (sequential direct) to 626× (SPIKE) faster than the warm-started per-frame loop. (b) One serpentine row reversal: the per-frame loop spikes to ~80 rad/s², the joint solve caps at ~15 with no aftershock chatter. (c) Vibration excitation (max joint jerk per raster sample, log scale) over the part: frame-by-frame (bottom) reproduces the engraved image as jerk ripple; the joint solve (top) keeps the interior quiet.

Scaling up: all of BONES-SEED on one GPU

The BONES-SEED dataset — 288 hours of motion, 142,220 clips at 120 fps, with G1 retargets produced by soma-retargeter's production per-frame loop — is the corpus-scale stress test. The whole dataset streams from its tarball (never extracted) through a vectorized BVH front end (exact-parity rewrite of soma's loader, ~10× faster) into length-bucketed batched trajectory solves of 64 clips at a time. End to end: ~3.0 hours on this box's single GPU — preprocessing all 124.5M frames takes 21 minutes with zero errors, the joint solve of all 142,220 clips the remaining 160 — versus ~15 hours projected for the production-style batched per-frame loop at its measured rate on the same GPU. (The corpus tail is brutal: the longest 8% of clips hold 25% of the frames, and earlier runs silently lost them to a solver-rebuild OOM that the final pipeline fixes.)

Where the speedup numbers come from

Different comparisons at different scales measure different things — this table is the reconciliation. The hundred-fold numbers compare our frame-parallel solve against the production pattern at its worst utilization: one clip, sequential over frames, no batching (the honest latency story — one clip in flight is exactly the interactive case, where frame-by-frame has no batching available). Batching 100 clips rescues frame-by-frame's GPU utilization; what it cannot rescue is the serial chain over frames. Batched-vs-batched, the joint solve is 10× on solve throughput and 5.5× end-to-end — and 5.5× is conservative twice over: the projection charges the per-frame loop for pure solve only (no disk IO, no readback, no writes) while our 160 min includes everything, and the joint solve now also carries the full contact/guard objective set that the production loop doesn't have. Notably the joint solve does more arithmetic per frame (40 LM iterations touching every frame + 16 CG iterations, vs 24 per-frame iterations): the win is utilization and launch amortization — one captured mega-graph over 57k frames versus ~105 sequential small launches with host round-trips.

comparisonjoint solveframe-by-frameratiowhat each side is
FR3, 120 frames × 4 trajs2.7 ms replay / 17.9 ms cold715 ms265× / 40×both batched across the 4 trajs, 16 iters both sides
G1 dance, 690 frames206 ms29.5 s143×FBF one-clip sequential, 24 iters/frame, warm-started
G1 walk, 1088 frames213 ms46.3 s217×same; targets exceed leg reach (workspace-boundary chatter)
corpus solve rate24,336 f/s2,378 f/s10.2×both batched (B=64×896 frames vs B=100), graph-captured vs per-frame stepped
corpus wall-clock160 min measured873 min projected5.5×ours end-to-end incl. all IO and the full objective set (v6); theirs pure-rate extrapolation

Quality is a genuinely two-sided story, so it gets the full protocol treatment (foot sliding/floating during source-labeled contact, ground penetration, smoothness, joint limits, keypoint faithfulness — conventions from ReActor/OmniRetarget/PHC/GMR). A qualitative audit of the comparison viewer caught real problems in earlier configs — an over-smoothed baseline with locked elbows, shoulder/elbow branch flips, and an LM stall on clips far from the world origin — root-caused to a free-joint tangent-convention mismatch whose Jacobian error grows with distance from the origin, and fixed upstream in the solver with body-centered base tangents (the pipeline keeps xy-centering and a symmetry-breaking seed as defense in depth; the flagged retry tail shrank from 111 clips to 21 across the whole corpus). The current numbers — the full 142,220-clip corpus solve (v6, quality-identical to v5 after the solver speedups), evaluated on the standard 14,222-clip subset: the plain retuned joint solve tracks to 10.7 mm at 5.7× lower peak acceleration than production. Adding the ported objectives — world-plane penalties on foot points plus a whole-body capsule-surface guard (IKObjectiveWorldPlaneCapsule, radii fitted to the G1 collision meshes), contact-gated stance pins (IKObjectiveFootContact + IKObjectiveFootSkate), and a rest-pose anchor (IKObjectiveJointReference, the branch-flip fix) — brings key-effector error to 1.9 mm (production: 1.4) with foot slide at 2.3 cm/s (production: 2.2), foot penetration below production's own rate (1.3% vs 3.7% of frames), and a 2–3× smoothness advantage. Body-surface contact is no longer a blind spot: penetration is now also measured over a per-link capsule set fitted to the G1 collision meshes, and there too the joint solve comes in below the production data (3.9% vs 5.2% of frames, at less than half the mean depth). The remaining body contact is concentrated in floor work (kneeling, crawling, sitting), where the scaled human pelvis target sits lower than the robot's own thigh radius allows — tracking and the surface guard trade off, and the viewer shows the equilibrium faithfully.

→ Open the side-by-side comparison viewer — the SOMA human source next to the shipped production retarget, our plain joint solve, and the joint solve with ported objectives, animating in sync on 13 curated clips. Watch the crawling and kneeling clips: production (which has no contact handling) digs into the floor; the plane objective holds ours out. Built on the Apache-2.0 seed-viewer's model assets.

Walking the accuracy–smoothness frontier

Is the quality table above one fixed tradeoff, or a knob? A knob: scaling the smoothness weights by λ while holding the contact/plane objectives fixed traces a frontier of tracking error against acceleration/jerk, re-solved on the 14,222-clip evaluation subset (~25–35 minutes per point on one GPU). Frame-by-frame solving has no such knob — its temporal regularity is whatever warm-starting leaves behind, and its acceleration spikes are noise, not a chosen tradeoff: points below this frontier.

Cross-library check: single-frame IK still holds up

Re-run of the earlier single-frame comparison (Newton IK paper protocol: random reachable targets from FK of uniform-random configurations; success = position < 5 mm and orientation < 0.05 rad). Original numbers were measured on an RTX 4090; these are on this box's RTX PRO 6000, so compare ratios, not absolutes.

Appendix

Design space: who solves the banded system how

Everyone in trajectory optimization sits at one of four points on how they solve the block-banded Gauss–Newton system (bandwidth 2(k+1)n−1 for k-order costs; the banded Newton step is O(T·k²n³) — linear in T, same structure as Riccati/DDP and factor-graph message passing):

ApproachWhoLinear algebraParallel over T?Batch?
Direct banded / RiccatiKOMO, GPMP2, Crocoddyl/Aligator, OCS2banded/block-tridiag Cholesky, exactno (sequential sweep)rare (CPU)
Iterative CG on normal eqspyroki/jaxls, MPCGPUmatrix-free or BSR SpMVyesyes
Quasi-Newton, no linear solvecuRobo (L-BFGS on B-spline knots)two-loop recursion onlyyesexcellent
SamplingSTOMP, MPPI, MJPCnonerollouts parallelexcellent

Newton's prototype ships the first two as selectable backends (direct is new territory for GPU-batch; jaxls has no banded direct solver — its only sparse direct option is CHOLMOD via a CPU host-callback). cuRobo never forms a Jacobian: temporal structure lives in its B-spline knots (16 knots × 7 DoF = 112 variables, small enough that sparsity stops mattering), with linear-ish convergence and ~100 fixed iterations in exchange. Parallel-in-time direct methods (associative-scan LQR, cyclic reduction) exist but only pay off for single very long trajectories; batch parallelism is the cheaper GPU win.

Warp assessment: what worked, gaps, proposed extensions

Worked out of the box (pinned 1.15.0.dev)

Gaps & bugs hit

Small warp extensions worth proposing

Benchmark protocol & caveats
Pointers