Solver Benchmarks¶
benchmarks/solver/ compares SSQP against a
handful of other OCP solvers on the same set of problems. Every solver formulates the identical problem through
ProblemWrapper, an abstract
interface (costs, constraints, finalize, solve, save) with one subclass per backend translating those calls
into that backend's own API, so a given problem stays defined once while every solver attempts the same costs and
constraints on it.
Six solvers are compared:
-
ssqp-admm/ssqp-hpipm: SSQP itself, using the two per-stage QP backends described in QP Solvers -
fatrop/ipopt: formulated through CasADi'sOptistack, solved with the respective plugin -
aligator: Aligator -
mim_solvers: mim_solvers' CSQP, built on Crocoddyl
All six are run to the same nominal tolerance (1e-2 by default, see
benchmark_config.py), even
though that tolerance bounds a different internal quantity for each (SSQP's per-stage KKT/constraint residual,
Fatrop/Ipopt's scaled KKT error, Aligator's primal-dual residual, CSQP's convergence gap), since reimplementing
every solver's own termination criterion for an exact match isn't practical.
On fairness
A fully fair, apples-to-apples comparison across six independent solver codebases is close to impossible: each has its own convergence criterion, its own QP backend and warm-starting strategy, its own default tuning, and its own years of separate engineering investment behind it. These benchmarks are not a claim that SSQP is faster or higher-quality than any of the others. The goal is to give a rough sense of where SSQP sits relative to the state of the art on a shared set of problems, not to crown a winner.
Known gaps
-
On the Go2 problems, we were unable to get JIT compilation working in a timely fashion, so a fair benchmark for Fatrop and Ipopt (which rely on it) wasn't possible for those.
-
On the contact-switching problems (Go2 Jump, Go2 Wheelie, Go2 Frog), we excluded mim_solvers, since we were unable to get it to converge at all.
-
On Go2 Frog specifically, we were also unable to get Aligator to converge, leaving only the two SSQP solvers.
-
On Duffing Local, Fatrop is missing at
m = 256for the two largest horizons (N = 500andN = 1000): JIT-compiling its formulation at that size requires more memory and time than a fair evaluation could accommodate, so those points were left out. -
On Duffing Global, whose all-to-all coupling makes for a much denser formulation, both Fatrop and Ipopt are missing entirely at
m = 256(every horizon) for the same reason.
Using the selector below, you may change the tolerance to view the corresponding plots for each problem.
Problem sizes¶
The problems range from Pendubot's long, low-dimensional swing-up to Go2 Frog's short but heavily constrained, contact-switching locomotion. Sizes below are per SSQP's own direct-transcription formulation (state = generalized position + velocity, control = generalized torque plus one force variable per active contact); a range means the value changes over the horizon with the contact schedule.
| Problem | Horizon | State Dimension | Control Dimension | Constraints per Stage | NLP size (vars / constraints) |
|---|---|---|---|---|---|
| Pendubot | 2500 / 2.5s | 4 | 2 | 6 (terminal: 4) | 15,004 / 15,004 |
| Go2 Standing | 100 / 1s | 37 | 30 | 72 (terminal: 27) | 6,737 / 7,227 |
| Go2 Jump | 150 / 1.5s | 37 | 18–27* | 51–78 (terminal: 42) | 9,367 / 10,932 |
| Go2 Wheelie | 200 / 2s | 37 | 24–30* | 66–84 (terminal: 39) | 12,957 / 15,405 |
| Go2 Frog | 300 / 3s | 37 | 18–30* | 48–84 (terminal: 30) | 18,841 / 21,342 |
| Panda Circle | 400 / 4s | 18 | 9 | 30 (terminal: 21) | 10,818 / 12,021 |
* Varies with the number of feet in contact at each stage: every active contact adds a 3D force variable to that stage's control vector, so the dimension shrinks during flight phases and grows back during stance.
Reading the plots¶
Each plot bars four quantities per solver, side by side:
-
Runtime, in milliseconds
-
Iterations, the outer solve loop's iteration count
-
Final objective, the converged cost
-
Constraint violation, summed over every stage of that stage's worst bound violation (position, velocity, torque, friction, and, for the direct-transcription solvers, the dynamics-transition defect), against a dashed line marking the shared tolerance
A fast, few-iteration solve is not a meaningful win if the trajectory it returns is infeasible, which is what the constraint violation panel is for. A solver that fails to converge for a given problem is labeled "failed".
Since it's summed across stages rather than reported per stage, the plotted constraint violation is not directly comparable to the problem tolerance, which bounds a per-stage quantity. On a long horizon, this sum can end up above the tolerance even for a solver that has actually converged, since it is adding up many small, individually in-tolerance violations rather than reporting a single one that exceeds it.
Running the benchmarks yourself¶
See the solver/ README for the full
setup (a Docker image is provided, since several of these solvers pull in their own heavy dependency chains).
Once set up:
plot_benchmarks.py then renders one problem's data/<solver>/<problem>.json results into the plots shown below:
The Coupled Oscillators problems below are swept over problem size instead, via
--duffing-m/--duffing-horizon (multiple values run a grid search), and rendered with plot_grid_search.py:
python run_benchmark.py duffing_local --all-solvers --duffing-m 2 4 16 64 256 --duffing-horizon 20 100 200 500 1000
python plot_grid_search.py duffing_local --duffing-m 2 4 16 64 256 --duffing-horizon 20 100 200 500 1000
Pendubot¶
The Pendubot Swing-Up problem: an underactuated double pendulum swinging from hanging to upright, actuated at the shoulder joint only.
Go2 Standing¶
The Go2 quadruped holding a fixed standing configuration, all four feet in contact throughout, with no other motion target. A short, contact-heavy but otherwise static regulation problem.
Go2 Jump¶
The Go2 Jump problem: stance, flight, stance, with all four feet leaving the ground for a scheduled window in the middle and landing back at a specified footprint. In this case, the front left foot has no contact and, therefore, should always remain in air.
Go2 Wheelie¶
The Go2 rearing up on its rear two feet: the rear feet stay in stance for the whole horizon, while the front two lift off for a window in the middle and come back down at the end. Unlike Go2 Jump, there's no target trajectory for the freed feet; the standing-configuration cost is what pulls them (and the rest of the robot) back down once the window closes.
Go2 Frog¶
A longer, four-hop bounding motion 2 meters forward, with staggered per-leg liftoff/landing and brief full-flight windows between hops rather than Go2 Jump's single synchronized flight phase. The same motion also appears, run directly rather than through the benchmark harness, as Go2 Frog in the Gallery.
mim_solvers and Aligator both failed to converge on this problem (see "Known gaps" above), so only the two SSQP solvers are meaningfully compared below.
Panda Circle¶
The Franka Panda tracing the circular (green) end effector path in task space via frame-pose tracking costs, additionally
constrained to the rectangular window (red) inscribed in that circle, same as
Panda Circle (Trajectory Generation), reformulated here in
ForwardDynamics mode (torque-actuated) so every backend solves an equivalent problem.
Coupled Oscillators¶
A scalable benchmark instead of a fixed-size one: m weakly-coupled Duffing oscillators (state
x = [q_1, ..., q_m, v_1, ..., v_m], one control force per oscillator), swept over both m and the horizon to see
how each solver scales with problem size. Two coupling topologies are benchmarked: duffing_local, where
oscillator i only interacts with its immediate neighbors (a discrete Laplacian), and duffing_global, where
every oscillator interacts with every other one through a sinusoidal coupling term.
Each plot below lines runtime (left axis, solid) and iterations (right axis, dashed) per solver, faceted by the other swept variable; a missing point means that solver wasn't run at that grid point, while a point where the solver ran but didn't converge is marked with a red X.







