API reference#
The public surface is import gato: the solver (BSQP), its configuration
(SolverParams), the controller and policy layers, the hypothesis batch, the
task generators, the simulation worlds, the robot builder and the dynamics
fingerprint. Everything below is rendered from the docstrings in the installed
package; the conventions they rely on are in the consumer contract.
Solver#
- gato.interface.module_name(plant, N, variant=None)[source]#
Import name of a built solver module: bsqpN{N}_{plant}[_{variant}].
- gato.interface.available(variant='default')[source]#
Discover built solver modules in this package.
variant=”default” (the plain solver), “fc” (contact-force controls) or “eh” (exact Hessian) -> {(plant, N): filename} for that variant; variant=”all” -> {(plant, N, variant): filename}.
- gato.interface.robot_info(plant_type)[source]#
Registry metadata for a plant ({nq, nv, ee_frame, urdf, floating_base, contact_frames, collision, codegen_key}), or {} if unregistered.
The registry (_registry.json) is written by gato.build / tools/regen_grid.py; robots registered with gato.register_robot are read from the per-host cache.
- gato.interface.robot_urdf(plant_type)[source]#
Absolute path of a registered robot’s URDF: the checkout’s copy for the vendored robots (examples/<robot>_description, with meshes), the wheel’s bundled copy (URDF only — the headers are pre-generated, so no meshes are needed), or the path recorded by gato.register_robot.
- class gato.interface.SolverStats(solve_time_us, sqp_iters, kkt_converged, final_merit, initial_merit, ls_num_iters, pcg_iters, pcg_times_us, min_merit, step_size, linsys='pcg', budget_hit=False, row_max_violation=None, row_sum_violation=None, admm_r_prim=None, admm_r_dual=None, admm_iters=None, admm_linsys_iters=None)[source]#
Bases:
objectPer-solve solver statistics (batch-shaped numpy arrays).
- Parameters:
solve_time_us (int)
sqp_iters (ndarray)
kkt_converged (ndarray)
final_merit (ndarray)
initial_merit (ndarray)
ls_num_iters (int)
pcg_iters (ndarray)
pcg_times_us (ndarray)
min_merit (ndarray)
step_size (ndarray)
linsys (str)
budget_hit (bool)
row_max_violation (ndarray)
row_sum_violation (ndarray)
admm_r_prim (ndarray)
admm_r_dual (ndarray)
admm_iters (ndarray)
admm_linsys_iters (ndarray)
- solve_time_us: int#
- sqp_iters: ndarray#
- kkt_converged: ndarray#
- final_merit: ndarray#
- initial_merit: ndarray#
- ls_num_iters: int#
- pcg_iters: ndarray#
- pcg_times_us: ndarray#
- min_merit: ndarray#
- step_size: ndarray#
- linsys: str = 'pcg'#
- budget_hit: bool = False#
- row_max_violation: ndarray = None#
- row_sum_violation: ndarray = None#
- admm_r_prim: ndarray = None#
- admm_r_dual: ndarray = None#
- admm_iters: ndarray = None#
- admm_linsys_iters: ndarray = None#
- class gato.interface.SolveResult(xu, solve_time_us, stats, nx, nu, N, n_actuated=0)[source]#
Bases:
objectResult of one batched solve.
xuis the flat batch of trajectories, one row per solve:[x_0, u_0, x_1, u_1, ..., x_{N-1}]with row lengthN*(nx+nu) - nu.- Parameters:
xu (ndarray)
solve_time_us (int)
stats (SolverStats)
nx (int)
nu (int)
N (int)
n_actuated (int)
- xu: ndarray#
- solve_time_us: int#
- stats: SolverStats#
- nx: int#
- nu: int#
- N: int#
- n_actuated: int = 0#
- property batch_size#
- property n_fc#
- u0(b=0)[source]#
First ACTUATED control of batch entry b — what an MPC applies (contact-force slots, if any, are excluded).
- control_at(k, b=0)[source]#
Actuated control at knot k (clamped to the last control knot) of batch entry b (contact-force slots, if any, are excluded).
- fc_at(k, b=0)[source]#
Contact-wrench slots at knot k of batch entry b ((n_fc,); empty on non-contact-force builds).
- property diverged#
(B,) bool — non-finite merit or trajectory.
- class gato.interface.BSQP(model_path, batch_size, N, dt, params=None, *, plant_type=None, variant=None, rho_batch=None, mu_batch=None, pcg_tol_batch=None, **overrides)[source]#
Bases:
objectBatched SQP trajectory optimizer over one compiled solver module.
- Parameters:
model_path – robot URDF (must be the one the module was codegen’d from).
batch_size – number of parallel solves B (runtime; any B >= 1).
N – horizon length — selects the module bsqpN{N}_{plant}[_{variant}].
dt – knot spacing [s].
params – gato.SolverParams (defaults = the paper/MPC set); keyword overrides of its fields may be passed directly (
BSQP(..., mu=5.0)).plant_type – plant name; None auto-detects from model_path against the built modules / registry.
variant – “default” | “fc” (contact-force controls appended to u) | “eh” (exact-Hessian SO-SQP). Variant modules are separate ABIs.
pcg_tol_batch (rho_batch / mu_batch /) – optional per-solve (B,) overrides.
Construction needs numpy and the module only. pinocchio is loaded lazily by the FK helpers (ee_pos) and the sim/estimator layers (
[test]extra).Warm start:
solve(x, ref, xu_warm=None)is STATELESS w.r.t. the trajectory — None seeds a hold at x; pass the previousSolveResult.xu(or let MPCController own the shifted warm start) to warm-start. The solver IS stateful across solves in rho adaptation, duals and ADMM state (reset_rho / reset_dual / reset).Cost precedence: SolverParams scalars < set_cost_weights (scalars) < set_cost_weights_per_knot (N,3) [ee, qd, u]; per-joint vectors (set_q_pos_cost / set_u_cost_vec) and the fc cost are orthogonal terms.
Reference layout:
refis (B, 6N): per knot [x, y, z, 0, 0, 0] — the EE position target (orientation slots are reserved, currently unused). Trajectory layout:xuis (B, N*(nx+nu) - nu) = [x_0, u_0, x_1, u_1, …, x_{N-1}] with x = [q; qd].- set_linsys(mode)[source]#
Pick the S·λ = γ path for subsequent solves: “pcg” | “bdsv” | “bdsv_first”.
Host-side and zero-cost — an MPC loop can switch it per step.
- set_admm_linsys(mode)[source]#
ADMM inner-loop linear solver: “pcg” (default) | “bdsv_factor”.
Measured defaults, rulings and provenance: docs/constraints.md (set_admm_linsys).
- property admm_linsys#
The ADMM inner-loop linear solver currently set (“pcg” | “bdsv_factor”).
- property admm_active#
True while any row group runs the ADMM mechanism (limits or appended).
- set_admm_exit(eps_abs=0.0, eps_rel=0.0, check_every=1)[source]#
Residual early exit of the ADMM inner loop. Per solve, after each inner iteration, the loop stops once every ADMM row group g has
r_prim_g <= eps_abs + eps_rel*max|z_g|andr_dual_g <= eps_abs + eps_rel*rho_g*max|z_g|on the iterate just produced (rows of a group share units; the test is per group so one group’s scale cannot excuse another’s) AND that iterate’s linear solve converged (PCG below its cap, or an OK direct factor): an unconverged inner solve cannot read as a fixed point.eps_abs = eps_rel = 0(default) is the fixed budget ofenable_limit_admm(iters=...), bit for bit; with a toleranceitersis the cap.check_every: inner iterations between host checks that break the launch loop once every batch entry has exited (0 = never; exited entries are skipped on-device regardless).stats.admm_itersandstats.admm_linsys_itersreport what ran.Measured defaults, rulings and provenance: docs/constraints.md (set_admm_exit).
- property admm_eps_abs#
Absolute tolerance of the ADMM residual exit (0 with the exit off).
- property admm_eps_rel#
Relative tolerance of the ADMM residual exit (0 with the exit off).
- property admm_check_every#
Host break period of the ADMM residual exit, in inner iterations.
- set_collect_stats(on)[source]#
Collect per-iteration solver stats (PCG timings via cudaEvents, line-search merits). On by default; MPC loops that do not read SolverStats can switch it off (two event records per SQP iteration).
- set_drho_batch(drho_batch, set_as_reset_default=True)[source]#
Per-solve trust-region rho ADAPTATION factor (B,) — the multiplier the line search applies on reject/accept (reset_rho restores the default).
- debug_setup_kkt(xu_B, xcur_B, eepos_goals_B)[source]#
Run setup_kkt ONLY on a trajectory and return the assembled KKT blocks {Q, R, q, r, A, B, c} (B, …) — the way the anchor/exact-Hessian gates pin cost claims directly instead of arguing from solve outcomes.
- exact_hessian_available()[source]#
True if the loaded module was compiled with -DGATO_EXACT_HESSIAN=ON.
- set_exact_hessian(on)[source]#
Toggle the SO-SQP stage-Hessian PSD projection for subsequent solves.
Measured defaults, rulings and provenance: docs/constraints.md (set_exact_hessian).
- solve(xcur_B, eepos_goals_B, xu_warm=None, *, budget_s=None)[source]#
Solve the batch from states
xcur_B(B, nx) toward the EE referenceeepos_goals_B(B, 6N) — see the class docstring for both layouts.budget_s: wall-clock budget for the SQP loop, in seconds. The loop runs the next iteration only while the time spent so far plus that iteration’s predicted cost (its measured cost in the previous budgeted solve; the most expensive iteration of this solve when there is none) still fits, and returns the current trajectory (the line search accepts descent steps only, so it is the best plan so far); the first iteration always runs. The cut is at the iteration boundary, never inside a kernel, so a budget is exceeded by at most one iteration.SolverStats.budget_hitreports a budget stop andsqp_itersthe iterations run. None (default): no budget,max_sqp_itersiterations, the path unchanged by this option.xu_warm(B, N*(nx+nu)-nu): the warm-start trajectory. None seeds a hold atxcur_B(a cold start; NOT the previous solution — the solver keeps no trajectory state; pass the lastSolveResult.xuto warm-start, or use MPCController). The caller’s array is never modified;SolveResult.xuis a fresh array.Arrays may be numpy, torch or JAX; the result’s
xuis of the same kind. CUDA tensors (torch on a CUDA device, JAX on a GPU) take the device path: inputs are read in place through their device pointer andxucomes back on the device through DLPack, with no host round trip; a devicexu_warmis used as given (its knot-0 states are replaced byxcur_B; the floating-base quaternion healing of the host path is not applied). CPU tensors go through the host path.
- set_warm(xu)[source]#
Seed the resident warm start: (xu_size,) broadcast to the batch or (B, xu_size); numpy (uploaded once) or a CUDA tensor (device copy).
- hold_warm(x)[source]#
Seed the resident warm start with hold-at-x (every knot’s state = x, zero controls): x a CUDA tensor, (nx,) broadcast or (B, nx).
- pred_err_warm(x)[source]#
‖x − knot 0 of the resident row 0‖ for a CUDA state x (nx,) (the controller’s warm-startedness signal; floating base with a unit base quaternion: the tangent norm). Returns a Python float.
- solve_warm(x, ref, *, budget_s=None)[source]#
Solve from the resident warm start (its knot-0 states replaced by x). x: CUDA (nx,) broadcast or (B, nx); ref: CUDA (6N,) broadcast or (B, 6N);
budget_sas in solve(). The solution stays resident and comes back asSolveResult.xuon the device in the caller’s kind.
- shift_warm(best_id=0, hold=False)[source]#
Next tick’s warm start from resident row
best_idinto every row: the plan advanced one knot with the last knot repeated (hold=False, the controller’s “shift”) or the plan as solved (hold=True).
- property row_mech#
The active enable_limit_* mechanism (“telemetry” | “barrier” | “admm” | “al”), or None when no limit mechanism is installed.
- enable_limit_telemetry()[source]#
Install the canonical limit row-groups (URDF position/velocity/torque boxes) in TELEMETRY mode: solves report each group’s true violation in
stats.row_{max,sum}_violation; trajectories stay bit-identical.Measured defaults, rulings and provenance: docs/constraints.md (enable_limit_telemetry).
- enable_limit_barrier(mu=0.003, delta=0.05)[source]#
Bind the limit row-groups to the RELAXED log-barrier mechanism (soft prior; C² barrier with bounded Hessian, quadratic extension within
delta).Measured defaults, rulings and provenance: docs/constraints.md (enable_limit_barrier).
- enable_limit_admm(rho=0.01, iters=10, *, rho_q=None, rho_qd=None, rho_u=None, eps_abs=None, eps_rel=None, check_every=1)[source]#
Bind the limit row-groups to the ADMM-projection mechanism: an OSQP-style inner loop per SQP iteration (
itersiterations, warm PCG or the reused direct factor, see set_admm_linsys) — the layer’s “approximately hard” mode;rhois the ADMM penalty, andrho_q/rho_qd/rho_uoverride it per box (position, velocity, torque rows; see set_row_group_rho: the proximal fold belongs at each block’s cost scale).eps_abs/eps_rel(either given) turn on the residual early exit of set_admm_exit, withitersas the cap.Measured defaults, rulings and provenance: docs/constraints.md (enable_limit_admm).
- enable_limit_al(rho=1.0, *, rho_q=None, rho_qd=None, rho_u=None)[source]#
Bind the limit row-groups to the PHR augmented-Lagrangian mechanism: the outer dual update runs once per solve on the final trajectory, so warm-started repeat solves are the outer loop (
rhofixed per enable,rho_q/rho_qd/rho_uoverride it per box; duals persist across solves, reset_dual() zeroes them).Measured defaults, rulings and provenance: docs/constraints.md (enable_limit_al).
- set_row_group_rho(g, rho)[source]#
Mechanism weight of row group
g(barrier mu, ADMM rho or AL rho, whichever mechanism the group runs;get_row_groups()[g]["rho"]reads it). The enable calls give the three limit boxes one weight; the ADMM proximal fold rho*G’G belongs at each block’s cost scale (measured October 16, 2026: rho 0.01 on the torque rows against the 2e-6 control cost over-regularizes the controls and slows the arm where no limit binds). ADMM re-initializes (z, y) on the next solve.Measured defaults, rulings and provenance: docs/constraints.md (enable_limit_admm).
- enable_ee_terminal_equality(target, rho=10.0)[source]#
Append an EE terminal-position equality row-group (final-knot EE position ==
target, on-device FK in the solver’s EE frame). The mechanism follows the active enable_limit_* mode; call it AFTER the mechanism enable.Measured defaults, rulings and provenance: docs/constraints.md (enable_ee_terminal_equality).
- get_row_groups()[source]#
List of installed row-group descriptors (dicts with kind/block/mech, knot mask, and per-row lo/hi bounds).
- get_row_duals()[source]#
AL multipliers dict {lam_hi, lam_lo}, each shaped (B, MAX_ROW_GROUPS, N, MAX_ROWS_PER_GROUP) in the dense row-state layout (group gi’s active slots are [:, gi, knot_lo:knot_hi, :n_rows]). Equality rows carry their signed multiplier in the lam_hi slot.
- get_admm_state()[source]#
ADMM state dict {z, y} (auxiliary/dual), same dense layout as get_row_duals(). y is the interval-constraint multiplier estimate.
- set_row_group_bounds(g, lo, hi)[source]#
Override group
g’s interval bounds (n_rows each);lo == hirows are always-active equalities under AL, ADMM’s auxiliary state re-clips next solve.Measured defaults, rulings and provenance: docs/constraints.md (set_row_group_bounds).
- set_row_group_mask(g, mask)[source]#
Per-knot row-activity mask for row group
g.maskis an (N, n_rows) boolean array (row i live at knot k), or (N,) for vector kinds (SOC cones, collision groups — gated per knot). The group’s knot window still applies first. All-True is the default behaviour, bitwise. An inactive (knot, row) is treated exactly like a knot below the group’sknot_lo: ADMM keeps its proximal fold but no z/y update, gradient, merit or residual; AL/barrier/telemetry contribute nothing and the AL dual of that slot is reset. ADMM re-inits z/y on the next solve after every call.Nonerestores all-True.
- set_row_group_soft(g, sigma)[source]#
Soft/slack toggle for group
g: sigma > 0 makes its rows ELASTIC (AL: the multiplier saturates at sigma; ADMM: smoothed projection); 0 = hard.Measured defaults, rulings and provenance: docs/constraints.md (set_row_group_soft).
- set_admm_merit(on=True)[source]#
Ablation toggle: include the AL-form ADMM constraint value y’(g - z) + (rho/2)|g - z|^2 (current row state) in the line-search merit. v1 ADMM’s merit is tracking-only, so the line search rejects steps that trade tracking for feasibility (measured: closed-loop MPC parks in conservative basins). Off by default — the exact v1 semantics; only read while ADMM mode is active.
Measured defaults, rulings and provenance: docs/constraints.md (set_admm_merit).
- set_admm_rho_adaptation(on=True)[source]#
OSQP-style ADMM rho adaptation (opt-in; default OFF = bitwise pre-adaptation path). One per-solve SCALAR multiplier on top of every ADMM group’s rho baseline (the bound per-group ratios — cone u-block 0.01 vs collision Q-block 1.0+ — are preserved), updated once per SQP iteration from the inner loop’s final residuals: adapt when r_prim/r_dual is imbalanced by >5x, step by sqrt(ratio), clamp to [1e-2, 1e2] (OSQP’s rule). The dual form is unscaled, so rho changes need no y-rescaling; the rho*G’G fold refreshes each SQP iteration. The scale persists across solves (warm rho, like the (z, y) dual warm start); toggling resets it to 1. Telemetry: get_admm_rho_scale().
Measured defaults, rulings and provenance: docs/constraints.md (set_admm_rho_adaptation).
- get_admm_rho_scale()[source]#
Per-solve adapted rho scale, shape (B,). Effective ADMM rho of a group = its rho baseline * this scale (1.0 = unadapted).
- add_lin_u_rows(C, d=None, lo=None, hi=None, mech=None, rho=None, delta=0.05, sigma=0.0, cone=False, knot_lo=0, knot_hi=None, admm_iters=0, equilibrate=False, normalize=True)[source]#
Append a LIN_U row-group: m rows
g = C @ u + don the control block (C shape (m, nu), FROZEN at a host-chosen configuration — the cross-term audit’s contact-frame rule for config-dependent maps).Measured defaults, rulings and provenance: docs/constraints.md (add_lin_u_rows).
- fc_slots(frame, part=None)[source]#
fc slot indices of one contact frame (index or baked frame name): the 6-wide block [n(3); f(3)] (world-aligned, about the frame origin), or just its
"n"(moment) /"f"(force) half. Feeds add_fc_box / set_fc_ref / SolveResult.fc_at slicing on multi-contact (fc-on-feet) plants.
- add_fc_box(lo, hi, slots=None, **kw)[source]#
Box rows on contact-force slots (GATO_CONTACT_FORCES builds only): selection LIN_U rows on control columns n_actuated+slots.
slotsindexes into the fc block (default: all n_fc slots); lo/hi broadcast. Pin the wrench torque rows of a point contact withadd_fc_box(0, 0, slots=range(3))(wrench layout is [n; f]). Extra kwargs go to add_lin_u_rows (mech/rho/knot range/…).
- add_fc_cone(frame, mu, mech=None, rho=None, form='soc', **kw)[source]#
Friction cone on one contact frame’s fc FORCE slots (fc builds): rows [mu*f_z; f_x; f_y] on
fc_slots(frame, "f")— the wrench is world-aligned, so the map is CONSTANT (no freezing at q, unlike the arm EE cone) and holds at every knot. Mask it to stance knots withset_row_group_mask. Returns the group index.
- property n_contact_rows#
3 rows per baked contact frame (the CONTACT_POS group width).
- contact_positions(q)[source]#
(n_frames, 3) world positions of the baked contact frames (pinocchio FK of the frame origins — the points the device rows and the fc wrench act on), in
contact_framesorder.
- add_contact_pos_rows(targets=None, lo=0.0, hi=0.0, mech=None, rho=None, delta=0.05, sigma=0.0, knot_lo=1, knot_hi=None, admm_iters=0)[source]#
Append a CONTACT_POS row-group: for every baked contact frame f and knot k, three rows g = p_f(q_k) - tgt_k[f] (world xyz of the frame origin, on-device FK over GRiD’s contact_frame_positions surface) with the interval
lo <= g <= hion the RESIDUAL. Defaults pin the frames to their targets (lo == hi == 0, an equality per knot); a one-sided z row (lo = clearance, hi = inf, other rows masked off) is a swing clearance.targetsis (N, n_rows), (n_frames, 3) or (n_rows,) (the latter two broadcast over knots; None = zeros = the raw position). Rows fold at EVERY knot of [knot_lo, knot_hi) (default knots 1..N-1: knot 0 is the measured state, a row there is unsatisfiable by any step) — mask (knot, foot) rows per tick with set_row_group_mask and retarget with set_row_group_targets. Returns the group index. Mechanism defaults follow enable_collision (rows fold onto the Q block: admm rho 1.0, al rho 1.0; the rho that actually enforces is measured in docs/constraints.md).
- set_row_group_targets(g, targets)[source]#
Per-knot target table of CONTACT_POS group
g: (N, n_rows), (n_frames, 3) or (n_rows,) (broadcast over knots). ADMM z/y re-init on the next solve (the residual moved).
- enable_u_cone(C, d=None, mech=None, rho=None, form='soc', facets=8, facet_scale='inscribed', **kw)[source]#
Cone constraint on a mapped control quantity g = C @ u + d .T), rows [mu*f_n; f_t1; f_t2], frozen at q).
Measured defaults, rulings and provenance: docs/constraints.md (enable_u_cone).
- set_collision_environment(spheres=None, capsules=None, cuboids=None, planes=None)[source]#
Upload the runtime obstacle set for the COLLISION clearance rows . Lists of tuples, one per obstacle (all in world frame, meters):
Measured defaults, rulings and provenance: docs/constraints.md (set_collision_environment).
- enable_collision(mech=None, margin=0.02, rho=None, delta=0.05, sigma=0.0, knot_lo=1, admm_iters=0)[source]#
Append THE collision clearance group (one max): per-sphere rows d_i(q_k) >= margin over knots [knot_lo, N] — d_i = signed clearance of collision sphere i (baked at codegen,
collision_res) to the nearest obstacle from set_collision_environment (call that FIRST). The covering spheres are already conservative (inflated by the spherizer), so margin is extra safety on top.Measured defaults, rulings and provenance: docs/constraints.md (enable_collision).
- get_collision_row_duals()[source]#
COLLISION-band AL duals: dict of (B, N, n_spheres) arrays (lam_hi unused — the rows are one-sided; lam_lo is the hinge dual).
- set_cost_weights(q_cost=None, qd_cost=None, u_cost=None, N_cost=None, q_lim_cost=None, vel_lim_cost=None, ctrl_lim_cost=None)[source]#
Update scalar cost weights at runtime (None keeps the current value).
- set_q_pos_cost(weight)[source]#
Joint-posture nullspace anchor: adds 0.5*weight*||q - q_nom||^2 as a RUNNING cost on the q-block (default 0 = the historic EE-only cost, bitwise-off).
The EE-position cost leaves a nullspace on redundant arms (iiwa14: wrist roll j7 has a ~zero EE-position Jacobian column); by default only the q-limit barrier anchors it. Where the operating posture sits near/outside the margin-shrunk limits (barrier limits = URDF limits - 0.1 rad, plant.cuh JOINT_LIMIT_MARGIN), set q_lim_cost=0 and use a small q_pos_cost toward set_q_nom(x0[:nq]) instead — anchors without the barrier field.
weightmay be a scalar or a length-nq array (per-joint anchor stiffness — tune light joints independently; see set_u_cost_vec for the matching effort-side knob and the closed-loop rate-limit story).
- set_u_cost_vec(weights=None)[source]#
Per-joint control effort weights (length n_actuated; None resets to the scalar u_cost). The per-joint knob that makes a joint’s commanded correction respect a discrete control loop: with effort nearly free (u_cost=1e-6 default) the optimal posture/velocity correction is deadbeat-aggressive, and on a near-massless joint (iiwa14 j7, J_eff ~ 1e-3 kg m^2) a plant-model inertia mismatch turns the 100 Hz loop into a growing Nyquist oscillation (PDDP round-5, 2026-08-02). Raising ONLY that joint’s effort weight (e.g. [1e-6]*6 + [3e-3]) softens its channel without degrading the arm’s tracking.
- set_q_nom(q_nom=None)[source]#
Posture target for set_q_pos_cost (length-nq array; None resets to zeros).
- set_fc_cost(weight)[source]#
Contact-force regularization 0.5*weight*||f_c||^2 per knot (GATO_CONTACT_FORCES modules only — the fc slots appended to every control; inert on default builds). Default on fc builds is 1e-2, NOT 0: unregularized fc is a free 6-DoF wrench actuator and the solve destabilizes (1e-3 is already marginal — measured 2026-08-02). Use large weights (~1e6) to effectively pin fc to zero, or 0 only together with fc box rows (add_fc_box).
- set_fc_ref(ref=None)[source]#
Contact-wrench reference for the fc regularization (GATO_CONTACT_FORCES modules only): the fc cost becomes 0.5*fc_cost*||f_c - ref||^2 per running knot.
refhas n_fc entries (wrench layout [n; f] per contact frame, world-aligned at the baked frame), shared across knots and batch rows. None/empty resets to zeros — bitwise the historic pure regularization.This is how a force SETPOINT enters the solve: ref = the desired contact wrench (e.g. [0,0,0, 0,0,+F] for an F-newton press reaction), fc_cost = the force-tracking weight. Pair with cone rows on the fc columns (add_lin_u_rows / enable_u_cone with a selection C) for friction limits.
- set_cost_weights_per_knot(knot_weights)[source]#
Per-knot [ee, qd, u] weight triples, shape (N, 3): overrides the scalar q/qd/u/N weights (terminal EE weight = row N-1’s ee entry). Enables via-points, terminal-only goals, and horizon masking at runtime.
Noneclears the table (back to the scalar weights).
- property model#
pinocchio Model for model_path (built on first use; needs the [test] extra).
- property data#
- property ee_frame_id#
Frame id of the codegen’d EE target frame, or None if the URDF lacks it.
- ee_pos(q, frame='ee')[source]#
Frame position via pinocchio FK.
frame=”ee”: the URDF ee_frame (fixed-joint child, e.g. tcp). Since GRiD e31f7bd the device FK (tracking cost AND EE row-groups) uses the named-target
*_EEcodegen, which INCLUDES the terminal fixed-joint origin — device == this frame to f32 precision (~1e-7). frame=”solver”: historical alias for the device frame; now identical to “ee” (the old dropped-origin convention is gone upstream). Any other string is a URDF frame name (e.g. a baked contact frame fromcontact_frames): its origin, the point the fc wrench acts about.
- reset()[source]#
Clear all solver state carried across solves: duals (AL/ADMM), the adapted trust-region rho, and the external-force buffer. (Trajectory warm starts are the caller’s — see solve().)
- sim_forward(xk, uk, sim_dt)[source]#
One plant step on the DEVICE integrator for every batch entry: (B, nx) x (B, nu) -> (B, nx) (knot-0 external wrench applies).
- set_f_ext_B(f_ext_B)[source]#
Upload the external-wrench band the dynamics see.
The GPU buffer is body-major: 6*NUM_BODIES per (solve, knot); each 6-slot is that body’s spatial force in its JOINT-LOCAL frame about the joint origin, Featherstone-ordered [angular(3); linear(3)] (verified vs pin.aba 2026-07-07). World wrenches go through gato.common.world_wrench_to_joint_local and are reordered — see hypotheses.world_wrench_to_gato_slot.
Accepted shapes (N = knot count; wrench k applies to interval [k, k+1]):
(B, 6): per-solve EE wrench, broadcast over knots(B, 6*NUM_BODIES): per-solve body-major, broadcast over knots(B, N, 6): per-knot EE wrench (scattered into the EE body slot)(B, N, 6*NUM_BODIES): per-knot body-major
A correctly-sized contiguous buffer is always uploaded (a short buffer would make the device copy over-read -> garbage wrench -> NaN dynamics).
Configuration settings for GATO experiments and benchmarks.
- class gato.config.SolverParams(max_sqp_iters=1, max_pcg_iters=200, pcg_tol=0.0001, solve_ratio=1.0, mu=10.0, rho=0.01, adapt_rho=True, linsys=None, q_cost=2.0, qd_cost=0.01, u_cost=2e-06, N_cost=50.0, q_lim_cost=0.01, vel_lim_cost=0.0, ctrl_lim_cost=0.0, exact_hessian=False)[source]#
Bases:
objectBSQP solver configuration. Immutable; derive variants with
.replace().SQP and linear system:
max_sqp_iters: SQP iterations per solve (1 = RTI/MPC; raise for one-shot).max_pcg_iters,pcg_tol: PCG budget / tolerance (pcg and bdsv_first paths).solve_ratio: fraction of the batch that must converge before early exit.mu: merit-function constraint weight.rho: trust-region / regularization floor (> 0: f32 bdsv returns garbage steps at rho=0);adapt_rholets the line search adapt it per solve.linsys: “pcg” | “bdsv” | “bdsv_first” | None (None = wired default: pcg fixed-base, bdsv floating-base, the static arm of the controller’s per-step policy; seegato.linsys_autotune.resolve_linsys).
Cost weights (grid_plant tracking cost):
q_cost(running EE position),N_cost(terminal EE position),qd_cost,u_cost, and the clamped-log-barrier limit weightsq_lim_cost/vel_lim_cost/ctrl_lim_cost. Per-knot and per-joint refinements are runtime setters on BSQP (set_cost_weights_per_knot,set_q_pos_cost,set_u_cost_vec,set_fc_cost/set_fc_ref) and take precedence over these.exact_hessian: SO-SQP stage-Hessian projection (needs an “eh” variant module).- Parameters:
max_sqp_iters (int)
max_pcg_iters (int)
pcg_tol (float)
solve_ratio (float)
mu (float)
rho (float)
adapt_rho (bool)
linsys (str | None)
q_cost (float)
qd_cost (float)
u_cost (float)
N_cost (float)
q_lim_cost (float)
vel_lim_cost (float)
ctrl_lim_cost (float)
exact_hessian (bool)
- max_sqp_iters: int = 1#
- max_pcg_iters: int = 200#
- pcg_tol: float = 0.0001#
- solve_ratio: float = 1.0#
- mu: float = 10.0#
- rho: float = 0.01#
- adapt_rho: bool = True#
- linsys: str | None = None#
- q_cost: float = 2.0#
- qd_cost: float = 0.01#
- u_cost: float = 2e-06#
- N_cost: float = 50.0#
- q_lim_cost: float = 0.01#
- vel_lim_cost: float = 0.0#
- ctrl_lim_cost: float = 0.0#
- exact_hessian: bool = False#
Controller and policy#
Task-agnostic batched MPC controller: one step(x, reference) per tick.
The controller owns ONLY the solver interaction — warm-start buffer, hypothesis batch hooks, best-of-batch selection, and the real-time-iteration time shift. It never integrates dynamics, never sleeps, and never paces itself: simulation and timing belong to the caller (a gym env, a hardware loop, or the legacy MPC_GATO driver).
- class gato.controller.StepResult(u, best_id, xu_best, solve, hypo_stats, pred_err=0.0, reseeded=False, fc=None, iterations=0, budget_hit=False, wall_s=0.0, admm_linsys='', limits='', row_viol=None)[source]#
Bases:
objectOne MPC tick’s outcome: the ACTUATED control to apply (
u), the winning trajectory, the raw SolveResult and the warm-startedness signal.- Parameters:
u (ndarray)
best_id (int)
xu_best (ndarray)
solve (SolveResult)
hypo_stats (dict | None)
pred_err (float)
reseeded (bool)
fc (ndarray)
iterations (int)
budget_hit (bool)
wall_s (float)
admm_linsys (str)
limits (str)
row_viol (ndarray)
- u: ndarray#
- best_id: int#
- xu_best: ndarray#
- solve: SolveResult#
- hypo_stats: dict | None#
- pred_err: float = 0.0#
- reseeded: bool = False#
- fc: ndarray = None#
- iterations: int = 0#
- budget_hit: bool = False#
- wall_s: float = 0.0#
- admm_linsys: str = ''#
- limits: str = ''#
- row_viol: ndarray = None#
- class gato.controller.MPCController(solver, *, hypotheses=None, warm_start='shift', reset_rho_each_step=True, linsys=None, bdsv_threshold=None, task_tag=None, reseed_threshold=None, admm_linsys=None, admm_threshold=None, limits=None, limits_kw=None)[source]#
Bases:
objectBatched-SQP MPC controller around a gato.BSQP solver.
- Parameters:
solver – a constructed gato.BSQP.
hypotheses – optional gato.hypotheses.HypothesisBatch whose batch_size matches the solver’s (each batch entry solves under its own hypothesis; reality picks the winner each tick).
warm_start – “shift” (default) rolls the winning trajectory forward one stage each tick, duplicating the final stage (real-time iteration); “hold” re-seeds every batch entry with the winning trajectory as-is.
reset_rho_each_step – restore the rho penalty to its configured default before each solve (line-search adaptation is per-solve state).
linsys – S·λ = γ path policy. “pcg”/”bdsv”/”bdsv_first” pin the solver’s path; “auto” picks per step from warm-startedness: pred_err = ‖x_measured − x_predicted‖ (the shifted warm start IS the model’s one-step prediction) — above bdsv_threshold, or after a PCG cap-out, the step is treated as cold and solved with “bdsv_first” (exact solve on the fresh linearization, PCG warm-started after); otherwise “pcg”. None (default) resolves to the wired per-base default: “auto” for fixed-base solvers and “bdsv” for floating-base solvers. This is a workload-dependent policy, not a speed guarantee. Pass the solver’s mode explicitly to keep it pinned.
bdsv_threshold – pred_err threshold for linsys=”auto”; defaults to 0.1 under auto (the CDF study’s [0.08, 0.17] deadline band — low τ trades a tighter max for a fatter p90). Calibrate per task from StepResult.pred_err (see tools/autotune_linsys.py).
task_tag – optional workload tag into the autotuned linsys table (linsys_tuning.json (written at runtime next to _registry.json, gitignored; $GATO_LINSYS_TUNING overrides the path), written by tools/autotune_linsys.py; $GATO_LINSYS_TUNING overrides the location). With linsys=None, a tuned (plant, N, task_tag) entry overrides the wired default; an explicit linsys arg always wins.
reseed_threshold – optional pred_err threshold above which the warm start is RE-SEEDED to a hold at the measured state (and the solver duals reset) before solving. None (default) = off. A warm start whose tail is inconsistent with the measured state can be a merit LOCAL MINIMUM — the line search rejects every step and the gap ratchets (measured on floating-base MPC, 2026-08-11); the hold seed is always consistent, if conservative. Uses the same pred_err signal as linsys=”auto”; a re-seeded step is treated as cold there. Floating tasks should set this; set it above bdsv_threshold (re-seeding is the bigger hammer).
admm_linsys – the ADMM inner loop’s linear solver policy, read only while a row group runs the ADMM mechanism (solver.admm_active). “pcg” / “bdsv_factor” pin solver.set_admm_linsys; “auto” picks per step from the SAME warm-startedness signal as linsys=”auto”: a cold step (pred_err above admm_threshold, a PCG cap-out on the previous step, or a re-seed) factors the Schur system once and back-solves per inner iteration, a warm step runs warm PCG per inner iteration (the inner loop then starts near its fixed point and the residual exit of solver.set_admm_exit stops it after a few iterations). None (default) resolves “auto”, with a tuned (plant, N, task_tag) entry’s admm_policy/admm_tau when present.
admm_threshold – pred_err threshold for admm_linsys=”auto”; defaults to bdsv_threshold (one signal, two switches) until a fitted value exists (tools/autotune_linsys.py –admm).
limits – the URDF joint limits (position, velocity, torque boxes) as constraint rows on the solver, installed by the controller. None (default): a fixed-base arm whose solver has no row groups yet gets the ADMM-projection mechanism with the measured defaults (per-box rho 0.1 / 0.03 / 1e-5 for the position / velocity / torque rows, cap 10 inner iterations, residual exit at eps_rel 1e-2; LIMIT_DEFAULTS, docs/constraints.md); a solver the caller already configured (any row group installed) is left as configured, as are floating-base solvers. “off”: install nothing. “admm” / “barrier” / “al” / “telemetry”: install that mechanism (re-installing the canonical limit groups; appended groups must come after). The mechanism in force is
self.limitsandStepResult.limits; each step reports the winning plan’s violation per row group inStepResult.row_viol. The raw solver stays explicit: only the controller carries this default.limits_kw – keyword overrides for the enable call behind
limits(e.g. dict(rho=0.02, eps_rel=1e-3) for “admm”, dict(mu=…) for “barrier”).
- LIMIT_DEFAULTS = {'admm': {'eps_rel': 0.01, 'iters': 10, 'rho': 0.01, 'rho_q': 0.1, 'rho_qd': 0.03, 'rho_u': 1e-05}, 'al': {}, 'barrier': {}, 'telemetry': {}}#
- reset(x0, *, xu_warm=None)[source]#
Reset solver state and seed the trajectory.
By default every knot holds x0 with zero control. An explicit
xu_warmhas shape (xu_size,) (broadcast) or (B, xu_size); its first state is replaced by x0. This permits reproducible non-default initial guesses without reaching into controller internals. Inputs are copied (numpy, torch or JAX, any device: reset stages through the host; the first device-path step() uploads the seed once). Floating guesses must have unit base quaternions.
- warmup(x_measured, reference)[source]#
Warm-up solve: seed the warm start with each batch entry’s own solution.
Unlike step(), this neither selects a winner nor time-shifts — every row keeps its own solved trajectory (each hypothesis’ solution warm-starts itself on the next step). Returns the SolveResult.
- step(x_measured, reference, dt_elapsed=None, *, budget_s=None)[source]#
One MPC tick: solve from the measured state, return the control.
- Parameters:
x_measured – (nx,) measured state.
reference – flat (6N,) EE-goal window (or (N, 6), flattened).
dt_elapsed – how long the PREVIOUS control was actually applied (used only for hypothesis scoring); defaults to solver.dt. Pass the REALIZED integration time, not wall time.
budget_s – wall-clock budget for this step, in seconds, measured from entry. The solver runs SQP iterations while the time spent plus the next iteration’s predicted cost (its measured cost in the previous budgeted solve) still fits the remainder, then returns the best plan so far (always at least one iteration; the cut is at an iteration boundary, so a budget is exceeded by at most one iteration). None (default):
max_sqp_itersiterations, the path unchanged by this option.StepResult.iterations,budget_hitandwall_sreport what happened.
x_measured/referencemay be numpy, torch or JAX arrays. When both are CUDA tensors (torch on a CUDA device, JAX on a GPU) and the controller runs no hypothesis batch, the step takes the device path: the warm start lives in the solver’s resident buffer, the inputs are read in place, andu,xu_bestandsolve.xucome back on the device with no host round trip (bit-identical plan to the host path; thepred_errscalar is reduced on the device and agrees to float rounding; the host path’s floating-base quaternion check is not applied). Otherwise the host path runs (CPU tensors are copied in; a hypothesis batch scores on the host) anducomes back in the caller’s kind. The two paths may alternate; the warm start follows.
MPCPolicy: an obs -> action adapter usable in any gymnasium env (or plain loop).
Pure numpy — no gymnasium import here. The policy holds its own clock and slides a reference window over it; the controller does the solving.
- class gato.policy.TrajectoryReference(traj_flat, dt_knot, N)[source]#
Bases:
objectSliding (6N,) window over a flat [x, y, z, 0, 0, 0]*T trajectory.
- class gato.policy.GoalReference(goal_xyz, N)[source]#
Bases:
objectConstant window: hold a single 3D EE goal over the whole horizon.
- class gato.policy.MPCPolicy(controller, reference, dt_step, obs_to_state=None, budget_s=None)[source]#
Bases:
objectCallable obs -> action wrapping an MPCController.
- Parameters:
controller – gato.MPCController.
reference – object with .window(t) -> (6N,) (TrajectoryReference/GoalReference).
dt_step – the env’s control period (how long each returned action is applied); also used as the hypothesis-scoring dt.
obs_to_state – maps the env observation to the (nx,) solver state (default: identity/asarray — for envs whose obs IS [q, dq]).
budget_s – per-call wall-clock budget handed to
MPCController.step(iterations while the next one still fits; None = unbudgeted).
Hypothesis batches: run B parallel model/solver hypotheses through the batched solver each MPC tick and let reality pick the winner.
This is the “batch-as-identity” API: the batch dimension carries hypotheses (disturbance guesses, solver-parameter spreads, …) rather than independent problems. Lifecycle per tick:
hypotheses.apply(solver, x_measured) # before solve: program the batch result = solver.solve(…) best = hypotheses.select(solver, result, x_prev, u_prev, x_measured, dt)
At batch_size == 1 both hooks are no-ops (select returns 0), so a controller built with hypotheses=None or B=1 behaves as a plain single-solve MPC.
- gato.hypotheses.world_wrench_to_gato_slot(model, data, ee_frame_id, q, f_world)[source]#
World-axes wrench at the EE frame origin -> GRiD’s last-body f_ext slot.
GRiD’s slot is the last body’s spatial force in the LAST JOINT’s local frame about the joint origin, Featherstone-ordered [angular(3); linear(3)] — verified against pin.aba to 1e-8 (fext_frame_probe 2026-07-07). An older version got both the frame chain and the [linear; angular] ordering wrong, which is why a seeded-truth hypothesis LOST to zero-wrench rollouts (~45% win rate).
- class gato.hypotheses.HypothesisBatch[source]#
Bases:
ABCB parallel hypotheses evaluated by the batched solver each MPC tick.
- batch_size: int#
- abstract apply(solver, x)[source]#
Sample/refresh B hypotheses and program them into the solver (set_f_ext_B, set_rho_penalty_batch, set_mu_batch, …).
- select(solver, result, x_prev, u_prev, x_meas, dt)[source]#
Pick the hypothesis that best explains reality; return its index.
Default scorer: roll the PREVIOUS state/control one step under each hypothesis’ dynamics on the GPU and compare to the measured next state:
x_next_B = solver.sim_forward(x_prev, u_prev, dt) # per-hypothesis f_ext best = argmin_b || x_next_B[b] - x_meas ||
Hypothesis types that don’t alter the dynamics (e.g. per-batch rho/mu spreads) should override select() and score from
result.stats(e.g. argmin final_merit) instead.
- class gato.hypotheses.ForceHypothesisBatch(estimator, model, ee_frame='EE')[source]#
Bases:
HypothesisBatchWrench-disturbance hypotheses driven by a ForceEstimator/CEMForceEstimator.
estimatoris duck-typed: generate_batch() -> (B, 6) WORLD-frame wrenches, update(best, errors, batch_used), reset(), get_stats(). The adapter owns the world->GATO frame transform (URDFee_frame-> parent-joint local wrench) and uploads the transformed batch via solver.set_f_ext_B.
- class gato.hypotheses.IdentifiedWrenchBatch(identifier, model, ee_frame='EE', n_actuated=None)[source]#
Bases:
HypothesisBatchB=1 “batch”: the wrench IDENTIFIED from the last one-step motion residual.
Structurally identical to ForceHypothesisBatch — same world->GATO frame transform, same
set_f_ext_Binjection — so an A/B against it isolates exactly one variable: how the wrench is obtained. ForceHypothesisBatch SAMPLES B candidates and lets a GPU rollout pick a winner; this SOLVES for the wrench with one least-squares fit and needs no batch.Lifecycle note: the fit needs (x_prev, u_prev, x_measured), which the controller supplies to
select()— i.e. AFTER the solve. The estimate therefore lands one tick later, in the nextapply(). That one-tick lag is inherent to any observer (the FE has it too: its batch is scored against the previous interval).- Parameters:
identifier – an OneStepWrenchIdentifier over the SOLVER model.
model – the solver’s pinocchio model (robot only).
ee_frame – URDF frame the wrench acts at.
n_actuated – width of the applied-torque head of the control vector (
solver.n_actuated); on contact-force buildsu_prevalso carries the solver’s fc tail, which is NOT an applied torque and must not enter the residual.
- apply(solver, x)[source]#
Sample/refresh B hypotheses and program them into the solver (set_f_ext_B, set_rho_penalty_batch, set_mu_batch, …).
- select(solver, result, x_prev, u_prev, x_meas, dt)[source]#
No-op: there is only one hypothesis.
The fit itself is driven at SENSOR rate by the driver (MPC_GATO._observe_substep), not from this tick-rate hook — the control-period motion sample is too coarse to identify a wrench from (9.4% vs 0.5% median error; see OneStepWrenchIdentifier.identify). A caller running its own sim loop should call identifier.identify() at its measurement rate instead.
Force-disturbance estimators: sample B wrench hypotheses, refit on feedback.
Both classes share the duck-typed estimator contract consumed by
gato.hypotheses.ForceHypothesisBatch:
generate_batch() -> (B, 6) world-frame wrenches
update(best_idx, prediction_errors, batch_used)
reset(); get_stats() -> dict
- class gato.estimators.ForceEstimator(batch_size, initial_radius=10.0, min_radius=1.0, max_radius=100.0, smoothing_factor=0.3, seed=0, alpha=0.5, beta=0.8)[source]#
Bases:
objectFibonacci-sphere external-wrench hypothesis sampler (batch-as-identity): each batch entry rolls out under one candidate wrench; the winner’s error drives the sampling radius (the paper’s force-estimator MPC demo).
- class gato.estimators.CEMForceEstimator(batch_size, force_sigma=10.0, torque_sigma=0.0, elite_frac=0.25, min_sigma=0.5, max_sigma=100.0, alpha_mean=0.6, alpha_cov=0.3, process_noise=0.1, momentum_beta=0.4, seed=None, axial_seeds=True, shrink_on_exploit=0.7, plateau_shrink=0.85, spike_expand=1.25)[source]#
Bases:
objectCross-Entropy Method (CEM) force/torque estimator for 6D wrenches.
Maintains a diagonal Gaussian N(mu, diag(sigma^2)) over 6D wrench (fx, fy, fz, mx, my, mz)
Generates a batch with exploitation seeds + exploration samples
Refits to elite samples based on prediction error
- Parameters:
batch_size (int)
force_sigma (float)
torque_sigma (float)
elite_frac (float)
min_sigma (float)
max_sigma (float)
alpha_mean (float)
alpha_cov (float)
process_noise (float)
momentum_beta (float)
seed (int | None)
axial_seeds (bool)
shrink_on_exploit (float)
plateau_shrink (float)
spike_expand (float)
- property Sigma: ndarray#
- generate_batch()[source]#
Returns array of shape (batch_size, 6).
Slots: - [0] current mean (mu) - [1] zero wrench - [2] momentum seed (mu + 0.5 * momentum) - [3:] samples from N(mu, Sigma)
- Return type:
ndarray
- update(best_idx, errors, batch_used=None)[source]#
Update distribution parameters using elite re-fitting.
- Parameters:
best_idx (int) – index of the best-performing hypothesis
errors (ndarray) – array of shape (B,) with lower = better
batch_used (ndarray | None) – the exact batch used for evaluating errors (B x 6)
- Return type:
None
- class gato.estimators.OneStepWrenchIdentifier(model, ee_frame='EE', alpha=0.5, damping=0.001, max_wrench=2000.0, mode='wrench', weight_tau=0.1)[source]#
Bases:
objectLeast-squares wrench identification from the one-step motion residual.
The sampling estimators above search for the wrench that best explains the observed motion; this one SOLVES for it. Over an interval where a constant torque tau was applied, the nominal (payload-free) model needs generalized force
rnea(q, dq, ddq_measured)to produce the motion that was actually observed, sotau_resid = rnea(q, dq, ddq_meas) - tau_applied
is exactly the generalized force contributed by everything the solver’s model omits. An external wrench w at the EE frame contributes
J^T w, so w is the least-squares solution ofJ^T w = tau_resid— 6 unknowns from nv equations, one linear solve, NO batch dimension.That is the point: the hypothesis batch spends B parallel solves searching a 6-D space that a single lstsq resolves directly. Accuracy is limited by the finite-difference ddq and by wrenches outside the EE-Jacobian range (which no EE wrench can explain), not by sample count.
Frame conventions match ForceEstimator so the two are interchangeable behind gato.hypotheses:
generate_batch()returns (1, 6) WORLD-frame wrenches ordered [force(3); torque(3)] at the EE frame origin.- Parameters:
model – the SOLVER’s pinocchio model (robot only — the whole point is that the payload is unmodeled).
ee_frame – URDF frame the wrench acts at.
alpha – EMA smoothing on the estimate (1.0 = raw per-tick fit). The one-step fit is unbiased but noisy — ddq is a finite difference.
damping – Tikhonov damping on the lstsq (0 = plain pseudo-inverse); regularizes wrench directions the Jacobian barely observes. Near kinematic singularities an undamped pseudo-inverse amplifies the measurement residual without bound, so the default is nonzero.
mode – ‘wrench’ (default) uses the full identified wrench; ‘weight’ estimates the payload WEIGHT — the gravity-aligned component filtered on weight_tau — which is the part a constant-over-the- horizon wrench model can legitimately extrapolate.
weight_tau – time constant [s] of the weight filter (mode=’weight’). Longer filters trade noise rejection for lag. Tune against a separate validation set; the default is not a universal optimum.
max_wrench – magnitude clamp on force/torque halves, a divergence guard. NOT a physical prior: on this task the true wrench swings over 34-1041 N (a swinging payload on an accelerating arm is mostly inertial reaction, not weight), so clamp well above the static weight or the observer will saturate exactly when it matters.
- batch_size = 1#
- identify(q, dq, dq_next, tau_applied, dt, q_next=None)[source]#
Fit the wrench explaining (q, dq) -> (q_next, dq_next) under tau_applied.
q/dq/dq_next/q_next are ROBOT-only (the solver model’s coordinates); tau_applied is the actuated torque held over the interval. Returns the raw fit (unsmoothed); the smoothed value lands in
self.estimate.★ ACCURACY IS ALL ABOUT THE INTERVAL.
ddqis the AVERAGE acceleration over [t, t+dt], so the dynamics must be evaluated at the interval MIDPOINT (second-order) rather than its start (first-order), and dt must be the SENSOR period, not the control period. Measured on the closed-loop pick-place task against an exact-acceleration observer:1 ms interval, midpoint -> 0.5% median error (usable) 10 ms interval, start -> 9.4% median error (diverges in the loop: a wrong wrench drives worse control, which corrupts the next fit)
so pass the shortest measurement interval available and give q_next.
- class gato.estimators.IdentifiedWrenchSampler(batch_size, model, ee_frame='EE', *, seed=0, initial_radius=10.0, min_radius=5.0, max_radius=150.0, weight_tau=0.1, wrench_alpha=0.5, damping=0.001, max_wrench=2000.0, inertial_rows=False)[source]#
Bases:
objectHypothesis batch centred on the identified payload weight (batch-as-identity, B >= 1).
Rows: [0] the filtered payload weight (OneStepWrenchIdentifier mode=’weight’), [1] zero (the robot-only model), [2:] Fibonacci-sphere perturbations of the weight’s force at an adaptive radius (grows when a perturbation wins the one-step rollout, shrinks when a core row does). Every hypothesis is the weight plus a bounded correction; at batch_size == 1 the batch is the weight alone (the wrench_id weight arm).
inertial_rows=True also adds the full identified wrench (mode=’wrench’) and blends weight + lambda*(full - weight). Off by default: those rows explain the last tick best, so they win the selection, but they extrapolate the payload’s inertial reaction to the arm’s own motion over the horizon and occasionally fling the arm (2-3 of 100 pick-place scenarios).
Why not the ForceEstimator: on the pick-place task its 20 N exploration ball never reaches a 15 kg payload’s reaction (hundreds of N), so it steers by selecting among small guesses; which sampler suits which task is measured in docs/figure-refresh-2026-10-01.md. Duck-typed like ForceEstimator for gato.hypotheses.ForceHypothesisBatch;
identifymust be fed at sensor rate (MPC_GATO._observe_substep does), as for wrench_id.- property n_updates#
Tasks, worlds and environments#
Arm tasks sized to the URDF limits (October 2026, the joint-feasible MPC work).
A reference that asks for several times the joint velocity limit has no
feasible tracking solution: the limit rows then only decide how the plan
fails. The tasks here are scaled from the limits instead. feasible_fig8
sets the figure-8 period so the joint speed the path demands is a chosen
fraction of the URDF velocity limit (ratio below 1: the arm can follow;
above 1: the limits must bind and a feasible plan lags the reference), with a
differential-IK pass along the path as the estimate of the demanded joint
speed. fig8_scenarios and pickplace_scenarios draw seeded scenario
sets around a plant’s workspace for the dev-set and holdout evaluations
(examples/benchmarks/limits_holdout.py); example 12 and the docs use the same
functions. Everything here is numpy plus pinocchio (imported inside the
functions); no solver, no GPU.
- gato.tasks.ee_frame_of(plant)[source]#
The plant’s registered end-effector frame (the fixed joint the device FK targets): ‘EE’ on the paper’s arms, the tool frame on the 0.1.2 arms.
- gato.tasks.urdf_limits(model)[source]#
{‘q_lo’, ‘q_hi’, ‘qd’, ‘u’} of a pinocchio model: the URDF <limit> tags (the physical limits the simulated plant is graded against; the solver’s rows carry margin-shrunk copies, see get_row_groups()).
- gato.tasks.ik_track(model, ee_frame, q0, path_xyz, *, first_iters=60, damping=0.02, max_step=0.2)[source]#
Differential IK along an end-effector position path: damped least squares on the frame’s position Jacobian, one update per path point (the first point gets
first_itersupdates so the path starts converged), each update clipped tomax_steprad so an unreachable target leaves a large error instead of a diverging configuration. Returns (q, err): joint positions (T, nq) and the position error per point (T,).
- gato.tasks.within_q_limits(model, qs, margin=0.15)[source]#
True when every configuration in
qs(T, nq) is at leastmargininside the URDF position limits (the limit rows themselves sit 0.1 rad inside; a task whose IK path runs along the position limit is not a velocity-limit task).
- gato.tasks.joint_speed_ratio(model, ee_frame, q0, path_xyz, dt)[source]#
Peak demanded joint speed along the IK path as a fraction of the URDF velocity limit: max over points and joints of
|qd_j| / qd_lim_j, plus the IK position error max (m) and whether the path stays inside the position limits (reachability checks).
- gato.tasks.feasible_fig8(model, ee_frame, q0, dt, *, ratio=0.8, cycles=1, **fig8_params)[source]#
Figure-8 parameters whose period makes the demanded peak joint speed
ratiotimes the URDF velocity limit (differential-IK estimate fromq0; the speed of a fixed path scales with 1/period, so one rescale lands the target).fig8_paramsoverride FIG8_DEFAULT_PARAMS (A_x, A_z, offset, theta; the period is the output). Returns dict(params, ratio_est, ik_err_max, within_q_limits): the parameters, the estimate after rescaling, the IK reach error along the path (metres; large = the path is not reachable) and whether the IK path stays inside the position limits.
- gato.tasks.fig8_scenarios(model, plant, n, *, seed, dt=0.01, ratio_range=(0.6, 1.6), ee_frame=None, ik_err_max=0.005)[source]#
nseeded figure-8 scenarios forplant: amplitude in FIG8_AMPLITUDE[plant], centre within 0.1 m of the plant’s centre of record, rotation 0 to 90 degrees, and the period set by feasible_fig8 for a speed ratio drawn uniformly inratio_range(below 1 the arm can follow, above 1 the velocity limits must bind). The episode starts ON the path:q0is the IK solution at the path’s first point from the ready pose with 0.05 rad jitter (q_ready), so the run measures steady tracking rather than the reach from the rest pose; fig8_reference runs two periods. Scenarios whose path the IK cannot reach withinik_err_maxand inside the position limits (Q_MARGIN), or whose rescaled estimate misses the drawn ratio by more than 15 % (the IK path jumped between configurations), are redrawn. Returns a list of dicts (id, plant, q0, params, ratio, ratio_est).
- gato.tasks.pickplace_scenarios(model, plant, n, *, seed, n_goals=3, ramps=(0.0, 0.6, 1.2), ee_frame=None, ik_err_max=0.005)[source]#
nseeded pick-and-place scenarios:n_goalsgoals drawn in the plant’s goal box (each reachable by IK from the ready pose withinik_err_maxand inside the position limits by Q_MARGIN), start pose = ready with 0.05 rad jitter, and a reference ramp (seconds of minimum-jerk travel between goals, run_mpc_goals’goal_ramp) drawn fromramps: 0 is the paper’s step reference, which asks for maximal acceleration and makes the torque box bind. Returns a list of dicts (id, plant, q0, goals, ramp).
- gato.tasks.fig8_reference(scenario, N, periods=2)[source]#
(flat trajectory, sim_time) for a figure-8 scenario:
periodscycles to run, padded with extra cycles so the horizon window never runs off the end of the reference.
Simulation worlds for the closed-loop harness.
The MPC evaluation loop (MPC_GATO._play_control) advances the plant one
sensor-rate substep at a time through a WORLD object:
q_next, dq_next = world.step(q, dq, u_robot, sim_dt)
PinocchioWorld— the historical in-process world: pinocchio RK4 with optional pendulum payload and constant external wrench. It is a thin adapter overMPC_GATO’s own machinery. The stepping order matches the pre-worlds harness; corrected pendulum initialization (September 27) is a separate simulation-protocol change, so historical pools are not interchangeable.MuJoCoWorld— an INDEPENDENT simulator for contact experiments: the same URDF loaded by MuJoCo (verified the same robot bygato.fingerprint(import gato.fingerprint as fp; fp.check(...)) — seetest_worlds.py), plus an optional contact plane. Uses MuJoCo’s own integrator/contact solver: plant-model mismatch vs the solver’s rigid fc model is deliberate.
MuJoCo gotchas this module encodes (found 2026-08-04):
the vendored URDF’s
<mujoco><compiler meshdir=...>block points at a nonexistent dir —MjSpec.meshdiroverrides it to the URDF’s own dir;MuJoCo does NOT exclude contacts between a world-welded base and its child link (the parent filter skips the world body), so the intersecting base/L1 meshes produce phantom self-contacts at some configurations that corrupt the dynamics (inertia-response off 100x at q=0). Robot geoms therefore get contype=1/conaffinity=2 and the plane contype=2/conaffinity=1: robot-robot never collides, robot-plane does.
- class gato.worlds.PinocchioWorld(mpc)[source]#
Bases:
objectThe historical pinocchio-RK4 world, bit-identical to the pre-worlds loop.
Constructed by
MPC_GATOitself (it owns the model/data/pendulum/f_ext state);steppreserves the exact call order of the old inline code: augment control -> refresh world-wrench -> rk4.
- gato.worlds.probe_tip_drop(urdf_path, q, ref_point, span=0.2, iters=24, size_xy=(0.3, 0.3))[source]#
Height of a reference point (e.g. the EE FRAME origin) above the robot’s lowest CONTACT point at configuration q, probed with MuJoCo’s own collision checker.
Why this exists: the EE frame origin is NOT the tool tip (~33 mm apart on the iiwa14 mesh). Geometry placed relative to the frame starts the tool in collision, and a “surface” reference is physically unreachable — a hidden press bias for every controller (measured 2026-08-09: +13-15 N on the wipe task). Probe once, define all contact geometry at the TIP.
Bisects the highest horizontal table top (finite box under ref_point’s xy) that does NOT touch the robot at q; returns ref_point[2] - that z. Exact for the evaluation world, since the probe uses the same collision model/masks as MuJoCoWorld.
span: search window below ref_point (the tool must clear a table span below the reference and touch one at it).
- class gato.worlds.MuJoCoWorld(urdf_path, plane=None, timestep=0.001, record_contact=False, floating=False)[source]#
Bases:
objectMuJoCo world: our URDF + an optional contact plane, MuJoCo’s integrator.
- Parameters:
urdf_path (str) – The SAME URDF the solver/pinocchio use. Model alignment is a test gate (
gato.fingerprint.checkon the contact-disabled model: aligned to 1e-7 on iiwa14).plane (None | dict) –
None= no contact geometry (free-space experiments). A dict adds a horizontal TABLE surface (a finite box — an infinite plane at EE height would swallow the robot base 0.63 m below it and produce MN-scale penetration forces; found the hard way):{"z": top_surface_height}plus optionalfriction(sliding coefficient, default 0.6),pos_xy(center, default (0.6, 0.2) — under the iiwa14 ‘ready’ workspace),size_xy(half-extents, default (0.25, 0.25)).timestep (float) – MuJoCo integrator step;
step(q, dq, u, sim_dt)asserts sim_dt matches.
Notes
stepwrites (q, dq) into MuJoCo state each call (the caller’s state is authoritative — it lets the harness inject disturbances), applies u as joint-space torque viaqfrc_applied, and returns copies.Deterministic: same (q, dq, u) sequence -> same trajectory (MuJoCo is deterministic on a fixed binary/machine; the warmstart cache is reset each call for state-purity).
last_contactafter each step: dict withncon,fn(total normal force, plane frame = world z),ft(tangential magnitude) andfn_by_body(normal force per robot body in contact, keyed by URDF link name — the per-foot split the fc-on-feet gates read) — the contact-task metrics feed.No pendulum / no constant-wrench support: those are PinocchioWorld features; MuJoCo experiments model disturbances as geometry.
- property params#
World parameters for the protocol stamp (do-not-mix discipline).
ArmTrackEnv: a gymnasium environment for torque-controlled arm EE tracking.
The env owns everything a controller should not: pinocchio RK4 integration, the ground-truth (unmodeled) external wrench, time, and the reward/termination logic. Pair it with gato.MPCPolicy for batch-robust MPC-as-policy, or drive it with any other torque policy.
- class gato.envs.ArmTrackEnv(*args, **kwargs)[source]#
Bases:
EnvTorque-controlled arm with RK4 substeps and an optional unmodeled EE wrench.
Observation: the state
[q, dq](Box,(nq+nv,)).Action: joint torques (Box bounded by the URDF effort limits), applied zero-order-hold for
ctrl_dt(int(ctrl_dt/sim_dt)RK4 substeps).Reward:
-||ee_pos(q) - reference.window(t)[0:3]||(0 if no reference).terminated: non-finite state. truncated:
t >= horizon_sorreference.done(t).info:
{"t", "ee_pos", "ref0", "tracking_err"}.
- metadata = {'render_modes': []}#
MPC_GATO: the closed-loop simulation driver used by the paper experiments.
This is a driver, not the controller: it owns the simulation world (pinocchio RK4 or MuJoCo) (including the unmodeled pendulum payload and ground-truth disturbance), the wall-clock pacing, task logic (figure-8 windowing / goal switching), and stat collection. The per-tick solver interaction lives in gato.controller.MPCController (warm start, hypothesis batch, best-of-batch selection, time shift) — users with their own sim or hardware loop should use MPCController/MPCPolicy directly.
- class gato.mpc_gato.MPC_GATO(model, model_path, N=32, dt=0.03125, batch_size=1, constant_f_ext=None, track_full_stats=False, plant_type=None, variant=None, pendulum_config=None, params=None, linsys=None, bdsv_threshold=None, reseed_threshold=None, admm_linsys=None, admm_threshold=None, limits=None, limits_kw=None, fe_seed=0, fc_config=None, wrench_id=None, world=None, estimator='fe')[source]#
Bases:
objectClosed-loop simulation DRIVER (paper experiments): pinocchio-RK4 / MuJoCo world + pacing + task loops around an MPCController. Not the controller — see gato.controller.MPCController / gato.policy.MPCPolicy for your own loop.
- property force_estimator#
The hypothesis estimator (None at batch_size == 1).
- property wrench_identifier#
the wrench_id identifier, or the ‘wid’ sampler (which identifies too); None for ‘fe’.
- Type:
The sensor-rate wrench observer to feed each substep
- setup_external_forces(constant_f_ext)[source]#
Setup ground-truth external forces for the simulation.
constant_f_ext = [force(3), torque(3)] in WORLD axes acting at the solver’s EE frame origin. It is re-expressed into the EE parent joint’s local frame at every sim substep (pin.aba’s fext[j] is joint-local, so a world-constant force must rotate with the configuration — the old code set it once, which silently turned it into a wrist-local force).
- run_mpc_fig8(x_start, fig8_traj, sim_dt=0.001, sim_time=5.0, pace_by_solve_time=True)[source]#
Run MPC tracking a figure-8 trajectory.
- pace_by_solve_time: if True (default, real-time MPC), advance the sim by the
measured wall-clock solve time each control step (a faster solver re-plans more often). If False, advance a fixed dt per solve — matches the CPU baseline harness for an apples-to-apples per-solve tracking comparison.
Returns (None, stats). goal_distances uses goal knot 1 (one step ahead); goal_distances_knot0 uses goal knot 0 (the baseline’s convention).
- run_mpc_goals(x_start, goals, sim_dt=0.001, goal_timeout=5.0, goal_threshold=0.05, velocity_threshold=1.0, velocity_norm=1, pace_by_solve_time=True, settle_time=0.0, goal_ramp=0.0)[source]#
Run MPC tracking discrete goal positions (pick-place style).
- Parameters:
x_start – Initial state (robot only, no pendulum)
goals – List of 3D goal positions np.array([x, y, z])
sim_dt – Simulation timestep
goal_timeout – Max time per goal before timeout
goal_threshold – Distance threshold for goal reached (m)
velocity_threshold – Velocity threshold for goal reached (rad/s)
settle_time – how long [s] the distance AND velocity gates must hold continuously before the goal counts as reached (0 = the paper’s instantaneous gate; a dwell rejects fly-throughs). The goal’s reach time is the end of the dwell.
goal_ramp – seconds over which the EE reference travels from the EE position at the goal switch to the new goal on a minimum-jerk profile, knot by knot along the horizon (0 = the paper’s step reference: every knot targets the goal at once, which asks for maximal acceleration). The gates still test the goal.
velocity_norm – norm order for the settling gate on dq (np.linalg.norm
ord): 1 = L1 sum over joints (historic default, strictest), 2 = Euclidean, np.inf = worst joint.pace_by_solve_time – if True (default, real-time MPC), advance the sim by the measured wall-clock solve time each control step — so the sim clock (timeouts,
goal_reached_times,time_to_all_reached) is really cumulative WALL time and runs are not reproducible. If False, advance a fixeddtper solve: the clock becomes physical task time (paper-comparable completion times, deterministic runs).
Returns (None, stats) with per-goal outcomes/reach times + tracking stats.
Robots: codegen, cache and fingerprint#
gato.build: one-call robot codegen + solver-module build.
import gato gato.build(“path/to/robot.urdf”, N=[32, 64]) # name defaults to the URDF stem gato.build(“panda.urdf”, name=”panda”, ee_frame=”panda_hand”)
Generates gato/dynamics/<name>/{grid.cuh, limits.cuh} from the URDF (via the
vendored GRiD code generator), registers the robot in python/gato/_registry.json,
and compiles the bsqpN{N}_<name> pybind modules with CMake. Afterwards
BSQP(model_path=urdf, N=..., plant_type=name) just works.
v1 targets a source checkout (the repo’s CMakeLists + the pinned grid-rbd wheel are required); modules are always built from a source tree (plan D10).
- gato.builder.rotated_inertial_origins(urdf_path)[source]#
List links with rotated inertial origins, which codegen accepts directly.
- gato.builder.canonicalize_inertials(urdf_path, out_path)[source]#
Write a copy of the URDF whose inertia tensors are expressed in the link frame: every
<inertial><origin rpy>becomes zero and its tensor is rotated exactly (I_link = R I R^T; the centre of massxyzis unchanged). The rigid-body model is identical (pinocchio’s dynamics agree to machine precision). This optional normalization is not required by codegen. Returns the rewritten link names.
- gato.builder.codegen(urdf_path, name, ee_frame='EE', algorithm_list=None, out_dir=None, register=True, collision_res=0.15, contact_frames=None, floating_base=False)[source]#
Generate gato/dynamics/<name>/{grid.cuh, limits.cuh} + register the robot.
Returns the registry metadata dict. This is the single codegen path — both gato.build() and tools/regen_grid.py go through it. algorithm_list=None emits GATO_ALGORITHMS (the consumed families; smallest header that builds every module); pass “all” for GRiD’s full profile, or an explicit list. out_dir/register let tests generate elsewhere without touching the repo.
collision_res: sphere spacing in meters for the grid_collision namespace. The URDF’s <collision> geometry is spherized at this resolution (GRiD’s spherizer; meshes need trimesh and scipy) and grid.cuh grows the grid_collision:: device ABI (config_free + the differentiable clearance family). REQUIRED for solver modules — the constraint layer (plant.cuh/rowgroups.cuh) compiles against grid_collision:: — so the default bakes it (0.15). None emits no collision code (headers-only / codegen-inspection use; bsqp modules will not compile against it). Coarser = fewer/fatter spheres; the sphere set IS the clearance row set, so keep it small (0.15 ⇒ ~30-45 spheres on the vendored arms).
contact_frames: list of URDF fixed-joint names to bake as contact frames: grid.cuh grows the f_ext_body[_jacobian_*] contact-wrench map. None (default) bakes [ee_frame] — every GATO plant carries its EE contact map; pass [] to opt out.
floating_base: parse/generate with a quaternion free-flyer root: the modules run the grid_plant SE(3) step/linearization path (SI-EULER integrator, tangent state cost). ee_frame is then the base-pose target frame (go2: imu_joint).
- gato.builder.build(urdf_path, name=None, N=(32,), ee_frame='EE', arch=None, jobs=4, build_dir=None, collision_res=0.15, contact_frames=None, floating_base=False, contact_forces=False, exact_hessian=False)[source]#
Codegen + compile the bsqpN{N}_<name> solver modules for a URDF.
- Parameters:
urdf_path – robot URDF (fixed-base serial chain, or floating_base=True for a quaternion free-flyer root — the solver runs the grid_plant SE(3) step/linearization path, SI-EULER integrator).
name – plant name (module suffix, dynamics dir); default = URDF stem.
N – iterable of horizon lengths to build.
ee_frame – fixed-joint name of the EE target frame (codegen + metrics).
arch – CMAKE_CUDA_ARCHITECTURES override (default: native detection).
jobs – parallel compile jobs (each TU pulls the large grid.cuh; keep small).
build_dir – CMake build tree (default <repo>/build). NOTE: the tree is reconfigured for exactly this (plant, N) request — a developer tree configured with a profile (-DGATO_RECEIPT_PROFILE=ON) loses that configuration; pass a dedicated directory to keep it.
contact_frames (collision_res /) – see codegen() — collision sphere spacing (enables the grid_collision clearance rows) and CL-3 contact-frame baking.
exact_hessian (contact_forces /) – build the “fc” (contact-force controls appended to u) or “eh” (exact-Hessian SO-SQP) module VARIANT instead of the default — a separate ABI with its own module name (bsqpN{N}_{name}_fc / _eh), loaded with BSQP(…, variant=…).
Returns: list of built (name, N) pairs.
Per-host cache of registered robots and compiled solver modules — the pip install gato path.
gato.register_robot(“myarm”, “arm.urdf”, N=[32]) # codegen only (seconds) gato.BSQP(“arm.urdf”, N=32, plant_type=”myarm”, …) # first use compiles (minutes, once)
- Layout (
GATO_CACHE_DIRor platformdirs’ user cache, e.g. ~/.cache/gato): manifest.json name -> robot entry (urdf hash, options, codegen key, meta) robots/<codegen_key>/<name>/ grid.cuh, limits.cuh (one dir per URDF+options+generator) modules/<name>/<module_key>/ bsqpN{N}_{name}[_variant].so + meta.json build/<name>/<module_key>/ CMake tree while compiling (removed on success)
A module key hashes everything that shapes the binary: the robot’s codegen key (URDF bytes, options, grid-rbd provenance), N and variant, the content identity of GATO’s native sources plus the dependency headers (tools/native_identity.py, the same identity every module embeds), and the toolchain (CUDA arch, nvcc, host compiler, Python ABI, pybind11). Unchanged inputs never recompile; any change is a miss, never a stale binary. The checkout’s modules in python/gato/ (the receipt profile) take precedence over the cache when both exist.
- gato._cache.native_root()[source]#
The CMake source tree solver modules compile from: the wheel’s bundled copy (gato/_native) or the source checkout around an editable install.
- Return type:
Path
- gato._cache.detect_arch()[source]#
CUDA architecture of the first GPU as CMake wants it (“120”), None if unknown.
- Return type:
str | None
- gato._cache.toolchain_identity(arch)[source]#
Everything outside the sources that shapes a binary (memoized per process).
- Parameters:
arch (str | None)
- Return type:
dict
- gato._cache.module_key(robot, N, variant, plant_root, arch)[source]#
Content key of one solver module + the readable inputs it was derived from.
- Parameters:
robot (dict)
N (int)
variant (str)
plant_root (Path)
arch (str | None)
- Return type:
tuple[str, dict]
- gato._cache.robot_entry(name, cache=None)[source]#
- Parameters:
name (str)
cache (Path | None)
- Return type:
dict | None
- gato._cache.resolve(name, cache=None)[source]#
(manifest or vendored entry, directory holding <name>/grid.cuh) or None.
- Parameters:
name (str)
cache (Path | None)
- Return type:
tuple[dict, Path] | None
- gato._cache.register_robot(name, urdf_path, *, N=(32,), ee_frame='EE', floating_base=False, collision_res=0.15, contact_frames=None, variant='default', build=False, cache_dir_override=None, arch=None, jobs=2, force=False)[source]#
Generate a robot’s dynamics headers into the per-host cache and record it in the manifest. Compiles the solver modules now only with
build=True; otherwise the firstgato.BSQP(..., plant_type=name)compiles (once).- Return type:
dict
- gato._cache.find_module(name, N, variant='default', cache=None)[source]#
The cached .so for (name, N, variant) if one was built from the CURRENT inputs (stale entries from older sources/toolchains are ignored).
- Parameters:
name (str)
N (int)
variant (str)
cache (Path | None)
- Return type:
Path | None
- gato._cache.ensure_module(name, N, variant='default', *, cache=None, arch=None, jobs=2, quiet=False)[source]#
Return the cached module for (name, N, variant), compiling it once if missing.
- Parameters:
name (str)
N (int)
variant (str)
cache (Path | None)
arch (str | None)
jobs (int)
quiet (bool)
- Return type:
Path
- gato._cache.load_module(name, N, variant='default', *, cache=None, build_if_missing=True)[source]#
Import the cached module for (name, N, variant) as
gato.<modname>; None when the robot is not registered.- Parameters:
name (str)
N (int)
variant (str)
cache (Path | None)
build_if_missing (bool)
- gato._cache.available_cached(cache=None)[source]#
{(plant, N, variant): filename} of cache modules built from the current inputs.
- Parameters:
cache (Path | None)
- Return type:
dict
Dynamics-fingerprint checker: is YOUR plant model the same robot as ours?
test/dynamics_fingerprint.json pins forward-dynamics accelerations
qdd(q, qd, u) at canonical probes per plant (gravity -9.81, f_ext = 0),
evaluated by the attested device dynamics. Any consumer evaluates its own
model at the same probes and compares per joint — the two models disagreeing
HERE will disagree in every closed loop built on them (a 2.6-3.2x per-joint
response ratio was the entire PDDP round-5 “anchor instability”).
Schema 2 (floating-base plants): q is the STORED configuration (nq, quat
xyzw for a free-flyer), qd/qdd are nv-sized, and u is the FULL
generalized force (nv — a consumer applies it as qfrc, zeros on the base
rows). Probes keep the base velocity at zero and the base orientation at
identity, so the base qdd rows are frame-convention-immune (MuJoCo’s
world-frame linear acceleration == pin’s local one there).
MuJoCo example (their side, one screen):
import gato.fingerprint as fp
def mj_qdd(q, qd, u):
data.qpos[:] = q; data.qvel[:] = qd; data.ctrl[:] = 0
data.qfrc_applied[:] = u
mujoco.mj_forward(model, data)
return data.qacc.copy()
print(fp.report(fp.check(mj_qdd, "iiwa14")))
check needs no GPU and no compiled modules — just the JSON.
- gato.fingerprint.check(qdd_fn, plant, path=None, rtol=0.05)[source]#
Evaluate
qdd_fn(q, qd, u) -> qddat every probe ofplant.Returns a dict: per-probe reference/actual/relative error, plus the per-joint response RATIO over the damping-immune inertia probes (the effective-inertia comparison — the number that catches armature/rotor/ missing-mass mismatches).
okis True when every probe agrees withinrtolof its scale.
- gato.fingerprint.check_solver(solver, path=None, rtol=0.0001)[source]#
Self-check: a BSQP instance’s device dynamics vs the committed table (regression gate; also verifies the vendored URDF is the pinned one).
- gato.fingerprint.solver_qdd_fn(solver)[source]#
Device qdd(q, qd, u_full) via one
sim_forwardSI-EULER step: v’ = v + dt * aba(q, v, tau), so (v’ - v)/dt recovers aba EXACTLY (no integration approximation — only f32 rounding). Used for floating plants, whose debug_contact_dynamics path is fixed-base-shaped;u_fullis the nv generalized force and only its ACTUATED tail reaches the device.
Linear-system policy and row kinds#
Per-problem linsys policy autotuning: fit / decide / persist / resolve.
The pcg-vs-bdsv verdict is WORKLOAD-specific: it is decided by the task’s warm-startedness distribution (how often, and how far, the measured state lands off the shifted prediction), not by the robot alone. The 08-12 CDF study proved linsys=”auto” (per-step selection on pred_err) matches or beats both pure arms on the fig8 arms tasks — but a different disturbance profile moves the right threshold, and a workload that is (almost) always cold or always warm is better served by a pure arm with no switching at all.
- Pipeline (probe legs are TIMING — quiet box only):
tools/autotune_linsys.py runs the task once under pure pcg (collecting per-solve pred_err + solve ms) and once under pure bdsv (flat cost).
fit_tau() estimates the pred_err where predicted pcg cost crosses the bdsv cost; decide_policy() turns that + the measured pred_err distribution into {pure pcg | pure bdsv | auto@tau}.
save_tuning() persists the entry (keyed plant|N|task_tag) to linsys_tuning.json next to this file (registry-adjacent, like _registry.json); override the location with $GATO_LINSYS_TUNING.
MPCController(task_tag=…) resolves at construction: explicit linsys arg > tuned entry > wired per-base default.
Everything in this module is host-only numpy (no GPU, no solver import) so the logic is testable in the cpu lane.
- gato.linsys_autotune.fit_tau(pred_err, solve_ms, bdsv_ms, n_bins=12)[source]#
Fit the pred_err threshold where predicted pcg cost crosses bdsv cost.
- Parameters:
pred_err – per-solve traces from a PURE-PCG probe run.
solve_ms – per-solve traces from a PURE-PCG probe run.
bdsv_ms – the flat per-solve bdsv cost (median of the bdsv probe run).
n_bins – quantile bins over pred_err for the cost fit.
- Returns (tau, diag):
- tau: crossing threshold, linearly interpolated between the bracketing
bin centers. None if the (monotone) pcg cost never reaches bdsv_ms (pcg wins everywhere probed); 0.0 if it exceeds bdsv_ms already in the first bin (bdsv wins everywhere probed).
- diag: dict with the binned fit (bin_center / bin_cost arrays, n per
bin) for reporting.
The pcg-cost-vs-pred_err relation is fit as binned medians made monotone non-decreasing with a running max (iterations grow with how cold the warm start is; the running max removes small-sample inversions without assuming a functional form).
- gato.linsys_autotune.decide_policy(pred_err, tau, lo=0.02, hi=0.98)[source]#
Turn a fitted tau + the probe’s pred_err distribution into a policy.
Fraction of probe solves that would run cold (pred_err > tau) decides: <= lo -> pure “pcg” (cold solves too rare to be worth switching), >= hi -> pure “bdsv” (warm solves too rare), else “auto”@tau. tau None (pcg never crossed bdsv) -> pure “pcg”; tau 0.0 (bdsv cheaper even warm) -> pure “bdsv”.
Returns {“policy”: mode, “tau”: float | None, “cold_frac”: float}.
- gato.linsys_autotune.save_tuning(plant, N, task_tag, entry, path=None)[source]#
Insert/replace the (plant, N, task_tag) entry; returns the file path.
entry should carry at least {“policy”, “tau”}; the autotune CLI adds provenance (date, SHA, probe stats).
- gato.linsys_autotune.lookup(plant, N, task_tag, path=None)[source]#
The tuned entry for (plant, N, task_tag), or None.
- gato.linsys_autotune.resolve_admm_linsys(admm_linsys=None, admm_threshold=None, *, fallback_tau=None, plant=None, N=None, task_tag=None, path=None)[source]#
Controller-side resolution of the ADMM inner-loop linsys policy: explicit > tuned entry (
admm_policy/admm_tau) > wired default by horizon (“pcg” below ADMM_FACTOR_MIN_N knots, “bdsv_factor” from there).Returns (admm_linsys, admm_threshold) with admm_linsys in {“pcg”, “bdsv_factor”, “auto”} and admm_threshold set iff auto. Under auto an unset threshold takes the tuned
admm_tau, elsefallback_tau(the controller passes its SQP bdsv_threshold: one warm-startedness signal, two switches), else 0.1.
- gato.linsys_autotune.resolve_linsys(floating_base, linsys=None, bdsv_threshold=None, plant=None, N=None, task_tag=None, path=None)[source]#
Controller-side resolution: explicit > tuned > wired default.
Returns (linsys, bdsv_threshold) with linsys in {“pcg”, “bdsv”, “bdsv_first”, “auto”} and bdsv_threshold set iff auto.
Row-group KIND and MECHANISM ids — the Python mirror of gato/bsqp/rowgroups.cuh
(enum Kind / enum Mechanism). BSQP.get_row_groups() reports these ids;
the constraint API (interface.py), the KKT certificate and the tests import
them from here instead of re-declaring literals.