Skip to content

Benchmarks

Time and memory to build each model and hand it to the solver, in specsolve and four other libraries. Nothing is solved, and CI checks that every library builds the same model (bench/models/).

Solver Show As Axes
Libraries
click to hide
drag on a chart to zoom every chart

Reading the charts

One panel per model, one line per library. The x axis counts columns: the variables that reach the solver. Solver picks the solver for every panel and the table.

  • specsolve is the thick violet line.
  • The grey line is the matrix floor: a finished matrix handed to the solver in one call, passModel for HiGHS and loadModel for Gurobi. Its gap to specsolve is everything specsolve does before the solver holds the model. Its memory includes its own copy of the arrays.
  • A point is a median, and its whisker is the middle half of the rounds. Overlapping whiskers are a tie (why the median).
  • A shaded funnel is a projection past the 30 s or 16 GB budget. A wide funnel is a guess.
  • ÷ specsolve puts specsolve at 1×, so a line at 3× is three times slower or larger. A point outside the window is named at the edge it crosses.
  • Drag a box on any chart to zoom them all, and the table with them. A drag along one side zooms one axis. Double-click or Reset zoom to see everything.
  • The table divides by specsolve. Purple is where specsolve is faster, orange where the other library is, and grey within 10%. >30 s and >16 GB are sizes the budget refused.

The models

Columns and rows count the variables and the constraints that reach the solver, one per value that exists. Every variable and constraint also runs over snapshot, and a model grows by its snapshots alone.

dispatch network nodal sector
for bounds and one balance: the shape an array library suits best a small PyPSA model: lines, stores, solar and wind, and pooled caps dispatch at nodes, where a technology exists only where it is installed each technology delivers one of five carriers, such as heat or hydrogen
variables p[generator] p[bus, tech], f[line], charge[store], discharge[store], soc[store] p[node, tech] p[node, tech]
constraints power_balance balance[bus], soc_balance[store], pool_cap[pool] balance[node] balance[node, carrier]
grouped sums — line → bus, store → bus, bus × tech → pool — —
dimensions generator 100 bus 50, tech 12, line 100, store 50 node 50, tech 12 node 50, tech 12, carrier 5
density 100% 25% 25% 25%
snapshots 2–100k 2–25k 2–20k 2–20k
columns per snapshot 100 400 150 150
rows per snapshot 1 150 50 125
nonzeros per snapshot 100 800 150 150

From 100k columns up, linopy's peak memory stays within 1.3× of specsolve's on HiGHS, except on network, the one model that sums through a grouping. There it is 2.7× at 1M columns, 1.75 GB against 0.65 GB, and the memory budget stops linopy at that size (#2039).

A script that solves one model

The charts leave out what a script pays to start: the interpreter, the import and the first build. This table times a new process from launch to exit, on HiGHS.

launch to exitdispatch
10k columns
network
10k columns
nodal
3k columns
sector
3k columns
specsolve0.46 s0.51 s0.47 s0.49 s
specsolve-in-memory0.46 s · 1.0x0.50 s · 1.0x0.46 s · 1.0x0.47 s · 1.0x
linopy0.96 s · 2.1x1.10 s · 2.1x0.92 s · 2.0x0.95 s · 1.9x
pyomo0.69 s · 1.5x0.89 s · 1.7x0.68 s · 1.4x0.74 s · 1.5x

A script that solves one small model with specsolve finishes in about half a second, about half of linopy's time (#2031).

Not measured yet

  • Sparsity other than a quarter. nodal, network and sector are sparse at one density, a quarter of the node and technology pairs. pixi run density sweeps nodal from dense to one pair in twelve, and no published run has taken that sweep.
  • Solve time. Every number stops when the model reaches the solver.

Method

Each measurement runs in its own process, with import excluded and teardown included, on an idle machine. A point is the median of nine rounds, or of five where a round takes over a second. Peak memory is ru_maxrss. A projection's two edges carry the last measured step forward, one at its cost per column and one at its growth rate. uv run --locked bench/reproduce.py runs it again with every version pinned, and the harness has every flag and caveat, such as the allocator cost on polars' peak (#896).