bench/ (#143) ships two cases on one sink. These are the gaps, in the order they would change a decision. Each is a small addition — the runner, the parity gate and the report are already case- and arm-agnostic.
1. A solver_direct arm
The biggest gap, because the LP file is not the shipped path. bench/ currently measures write_lp against to_file(io_api='lp-polars'), and the phase split (see the build-perf issue) shows the emit phase is at parity while build is 6-10x slower. On solver_direct there is no emit phase, so the ratio users actually experience is the build ratio — unmeasured today.
Wants a matching linopy arm (io_api="direct" / to_highspy) and a peak that separates the build side from HiGHS's own model, which hard rule 4 exempts. The earlier harness measured 5.76 GB end-to-end at 35.6M variables, dominated by the solver.
2. storage — the roll self-join
roll/shift is the only locality class in the ceiling with no eager analogue in cost: xarray shifts an array, we join on dim.ord - k. It is also the shape that makes real energy models large (temporal coupling), so its absence from the ladder is the most misleading thing about the current numbers. examples/storage.yaml is the model; it needs a scaling twin under bench/models/ with data-driven dimension members.
3. A MILP through solver_direct
Solve time dwarfs build, so this measures the handoff rather than the build — a different question, and the one where being 2x slower to build is irrelevant. tests/test_milp.py has the shape. Also the only case where vtype streaming gets exercised at scale.
4. A where-density sweep
Masks are row absence here and NaN-dense in xarray. That is a claim ARCHITECTURE.md makes and nothing has ever measured. Sweeping mask density (100% / 30% / 5% active) is the one axis where the gap should be qualitative rather than 2x — the eager lane pays for every masked-out cell and we pay for none.
The dispatch model already carries where: p_max > 0; the generator only needs to zero a configurable fraction of p_max, and the ladder a density parameter.
5. A hand-written highspy/CSR arm — the speed-of-light floor
Without it, "2x slower than linopy" has no denominator: there is no way to tell engine overhead from the irreducible cost of emitting 10^7 coefficients. With it, the sentence becomes "we are at Nx the floor and linopy is at Mx", which is a claim about engineering rather than a ranking.
Not a fair modelling comparison — it hardcodes one model — which is exactly why it belongs as a floor rather than as an arm in the headline table.
6. A 10^8-variable capability run
The old harness's 107M-variable run (0.57 GB peak, a model whose dense build does not fit on the machine) is the single most persuasive number this project has ever produced, and it is currently in docs/benchmarks.md under "not reproducible". One rung above l on the existing ladder gets it back, by hand rather than in the default run.
Explicitly not planned
- CI regression gating. Shared runners make peak RSS noisy enough that the gate either fails constantly or is loosened until it catches nothing.
- Pyomo / PuLP / Calliope arms. Beating a Python-object modelling layer on build time proves nothing anyone disputes, and the high-level frameworks would mostly be measuring their own data prep. If it comes up, a paragraph in the doc answers it more honestly than a bar chart.
bench/(#143) ships two cases on one sink. These are the gaps, in the order they would change a decision. Each is a small addition — the runner, the parity gate and the report are already case- and arm-agnostic.1. A
solver_directarmThe biggest gap, because the LP file is not the shipped path.
bench/currently measureswrite_lpagainstto_file(io_api='lp-polars'), and the phase split (see the build-perf issue) shows the emit phase is at parity while build is 6-10x slower. Onsolver_directthere is no emit phase, so the ratio users actually experience is the build ratio — unmeasured today.Wants a matching linopy arm (
io_api="direct"/to_highspy) and a peak that separates the build side from HiGHS's own model, which hard rule 4 exempts. The earlier harness measured 5.76 GB end-to-end at 35.6M variables, dominated by the solver.2.
storage— therollself-joinroll/shiftis the only locality class in the ceiling with no eager analogue in cost: xarray shifts an array, we join ondim.ord - k. It is also the shape that makes real energy models large (temporal coupling), so its absence from the ladder is the most misleading thing about the current numbers.examples/storage.yamlis the model; it needs a scaling twin underbench/models/with data-driven dimension members.3. A MILP through
solver_directSolve time dwarfs build, so this measures the handoff rather than the build — a different question, and the one where being 2x slower to build is irrelevant.
tests/test_milp.pyhas the shape. Also the only case wherevtypestreaming gets exercised at scale.4. A
where-density sweepMasks are row absence here and NaN-dense in xarray. That is a claim ARCHITECTURE.md makes and nothing has ever measured. Sweeping mask density (100% / 30% / 5% active) is the one axis where the gap should be qualitative rather than 2x — the eager lane pays for every masked-out cell and we pay for none.
The
dispatchmodel already carrieswhere: p_max > 0; the generator only needs to zero a configurable fraction ofp_max, and the ladder a density parameter.5. A hand-written highspy/CSR arm — the speed-of-light floor
Without it, "2x slower than linopy" has no denominator: there is no way to tell engine overhead from the irreducible cost of emitting 10^7 coefficients. With it, the sentence becomes "we are at Nx the floor and linopy is at Mx", which is a claim about engineering rather than a ranking.
Not a fair modelling comparison — it hardcodes one model — which is exactly why it belongs as a floor rather than as an arm in the headline table.
6. A 10^8-variable capability run
The old harness's 107M-variable run (0.57 GB peak, a model whose dense build does not fit on the machine) is the single most persuasive number this project has ever produced, and it is currently in
docs/benchmarks.mdunder "not reproducible". One rung abovelon the existing ladder gets it back, by hand rather than in the default run.Explicitly not planned