Constrained IK (hard limits)#
Mink solves a QP each control step: minimise task error subject to hard
inequalities G Δq ≤ h. mink-warp mirrors that via
ConstrainedSolver — a batched OSQP-style ADMM inner
solve on the same normal equations as DLSSolver.
Problem#
Per world, after stacking tasks into \(H, c\):
Velocity is \(v = \Delta q / dt\) as usual.
Two solve paths (auto-selected)#
Path |
When |
Properties |
|---|---|---|
Box ADMM |
Every limit is a per-dof interval ( |
Fast; exact box feasibility at every ADMM iteration (even if truncated) |
General inequality ADMM |
Any inequality-only limit (e.g. |
Reduced Schur-normal OSQP-ADMM; feasibility improves with |
Both paths factor a SPD matrix once per solve (tile Cholesky) and run a fixed number of ADMM iterations. They work on CUDA and CPU (Warp LLVM backend).
Built-in limits#
Limit |
Form |
|---|---|
|
|
|
|
Normal-velocity rows from |
|
Constant |
Each limit exposes either a box (apply_box()) or
dense rows (scatter_inequalities()), matching
Mink’s stacked G Δq ≤ h. See Limits.
Functional API (Mink-shaped)#
Omit limits for unconstrained DLS (historical default):
v = mw.solve_ik(cfg, tasks, dt) # no hard limits
Pass limits to auto-build a ConstrainedSolver:
# Mink's limits=None → default ConfigurationLimit
v = mw.solve_ik(cfg, tasks, dt, limits=None)
# Explicit stack
v = mw.solve_ik(
cfg, tasks, dt,
limits=[mw.ConfigurationLimit(model), mw.VelocityLimit(model, 3.0)],
)
# Unconstrained despite passing limits= (empty list)
v = mw.solve_ik(cfg, tasks, dt, limits=[])
limits= is only honoured when solver=None. Passing both solver= and
limits= raises — configure limits on the solver you construct, or drop
solver=.
Explicit solver#
solver = mw.ConstrainedSolver(
cfg,
limits=[mw.ConfigurationLimit(model, gain=0.95)],
admm_iters=30,
damping=1e-12,
)
v = solver.solve(tasks, dt=0.01)
Tuning knobs: admm_iters, rho_scale / rho_min / rho_max, alpha
(over-relaxation), sigma (general path SPD floor), use_inequalities.
General inequalities#
Use LinearInequalityLimit for constant half-spaces Mink’s
per-dof box cannot express:
import numpy as np
# e.g. n @ dq <= h0 (one row)
n = np.zeros(model.nv, dtype=np.float32)
n[2] = 1.0
lim = mw.LinearInequalityLimit(n[None, :], [0.05])
solver = mw.ConstrainedSolver(cfg, limits=[lim], admm_iters=200)
v = solver.solve(tasks, dt=0.01)
Presence of an inequality-only limit auto-selects the general path. For parity/debugging you can force box limits through the dense path:
mw.ConstrainedSolver(
cfg,
limits=[mw.ConfigurationLimit(model)],
use_inequalities=True,
admm_iters=80,
)
Custom limits#
For configuration-dependent rows (collision normals that move with q),
subclass Limit and implement
scatter_inequalities() (set box_capable = False
if there is no per-dof box form).
Box vs general — when to care#
Control loops with joint + velocity caps: default box path; raise
admm_itersonly if you need closer agreement with the true QP optimum.Half-spaces / coupled dof bounds:
LinearInequalityLimitor a customLimit; budget moreadmm_iters— feasibility is asymptotic.Mink parity tests:
ConfigurationLimitbox path matches minkdaqp;use_inequalities=Truere-expresses the same rows as denseG Δq ≤ h.
Validation#
tests/test_constrained_solver.py— box path vs minktests/test_inequality_constraints.py— general path, mixed limitstests/test_admm_reference.py— NumPy ADMM oraclebenchmarks/bench_constrained.py— throughput + violation metricsbenchmarks/bench_osqp.py— inner ADMM vs OSQP on standard QPs