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/).
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,
passModelfor HiGHS andloadModelfor 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 sand>16 GBare 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 exit | dispatch 10k columns | network 10k columns | nodal 3k columns | sector 3k columns |
|---|---|---|---|---|
| specsolve | 0.46 s | 0.51 s | 0.47 s | 0.49 s |
| specsolve-in-memory | 0.46 s · 1.0x | 0.50 s · 1.0x | 0.46 s · 1.0x | 0.47 s · 1.0x |
| linopy | 0.96 s · 2.1x | 1.10 s · 2.1x | 0.92 s · 2.0x | 0.95 s · 1.9x |
| pyomo | 0.69 s · 1.5x | 0.89 s · 1.7x | 0.68 s · 1.4x | 0.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,networkandsectorare sparse at one density, a quarter of the node and technology pairs.pixi run densitysweepsnodalfrom 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).