Skip to content

Examples

Every model in the repo, what it says, and what it exercises. Three questions, in the order you probably have them: can it say my model? · is it readable? · does it get the right answer? The first and the third are the two tables on this page; the second is each model page itself, where the file sits beside the same model written on another stack.

Every page starts from data in the shape the call wants, and Preparing the data is where that shape comes from.

Every model

Teaching examples

dispatch Least-cost generation against a load profile — the smallest model that is still a model.
storage Dispatch plus a battery, and the only construct in the language whose cost is not obviously linear.
transport A network: generators sit on buses, lines connect buses, and power balances at every bus.
piecewise Per-generator convex cost curves, expanded into a λ-formulation.
special-ordered sets A piecewise-linear cost curve stated as a special-ordered set — piecewise with one line changed, handed to the solver as a set it branches on itself.
monthly budget A cap on what each technology may generate per calendar month — an aggregate over a coarser grouping of time, written with the same operator that places a generator on a bus.
multi-period Capacity decided once per investment period, binding at every snapshot inside it — and the periods need not be the same size.
seasons A store that cycles inside each season rather than across the horizon, with seasons of different lengths — one balance row says it, because the wrap is the season's own and no level is carried from one season into the next.
reserves Energy and reserve co-optimization on a two-bus grid: offers are (generator, market, tranche) triples, reserve zones overlap, and one line dangles. The model exists to prove a claim — every many-to-many shape the language covers, in one instance, each one load-bearing.
walkthrough The dispatch model plus a macro and a named expression — the one used to print every pipeline stage.

The PyPSA ladder

rung 1 — transport PyPSA linear optimal power flow, first rung: transport model, linear marginal cost, no KVL.
rung 2 — ramp limits Rung 1 plus a limit on how fast each generator may change output between snapshots.
rung 3 — storage Rung 2 plus a StorageUnit carrying energy between snapshots.
rung 4 — cyclic storage Rung 3 with the horizon closed on itself: the first snapshot's state of charge carries over from the last.
rung 5 — KVL Passive AC lines: flow is decided by physics, not chosen. The last rung of the ladder.
transmission losses The loss on a line is r · s². PyPSA approximates it from below with a fan of tangents.
rung 6 — AC-DC, two coordinates A meshed AC–DC network under a CO₂ budget. PyPSA's own ac-dc-meshed example.

PyPSA components and modes

unit commitment Which generators are on, not just how much they produce — a binary per generator per snapshot, with start-up and shut-down charges.
minimum up and down times A unit that has started must stay on; one that has stopped must stay off.
linearized unit commitment A unit may be committed by a third. PyPSA ships this as a mode, not as a debugging convenience.
global limits Limits that hold over a whole set at once: an energy total, a capacity at one bus, and the built network measured twice.
multi-link One Link, one input bus, several output buses, each output derated by its own efficiency — PyPSA's spelling for a CHP plant, an electrolyser with waste heat, any conversion with more than one product.
modular capacity Capacity that comes in whole modules: an integer count decides it, not a continuous bound.
energy totals A generator's dispatch reduced over every snapshot and bounded: a contracted delivery, a reservoir's season.
fixed by data A row of data that is present pins its variable; a row that is absent leaves it free.
spillage A hydro unit takes inflow it did not choose, and spills what neither turbine nor reservoir can absorb.
the Store component The component every sector-coupled PyPSA model uses for hydrogen, heat and gas.
mixed cycling PyPSA's cyclic_state_of_charge is a per-unit flag, so one network runs both regimes at once: a unit that must end each horizon where it began, beside one handed a level it may simply spend.
link delay A shipment is the input shifted along time: withdrawn at one snapshot, delivered at another, derated on the way.
multi-period investment A build year and a lifetime decide which rows an asset appears in, and each period's costs carry its own discount.
committable and extendable A minimum output that is a share of a capacity still being decided: two variables multiplied, and one constant to take them apart.
growth limit A cap on new capacity per investment period, which grows with the period before it. The first shift in the corpus along an axis that is not time-of-day.
stochastic scenarios Three futures over one network: the fleet is built before anyone knows which arrives, and dispatched after.
CVaR risk preference The same three futures as the stochastic rung, planned against the tail as well as the expectation.

Published optima

Dantzig transport Dantzig's transportation problem — GAMS model library #1, and the oldest LP in the corpus.
Dantzig, economies of scale GAMS model library trnspwl: the same shipping problem, but a big consignment is cheaper per unit — cost grows as sqrt(x), not linearly.
Stigler's diet The cheapest way to eat for a year and stay alive. 77 foods, 9 nutrients, 1939 prices.
Facility location Where do you put the warehouses? Open a set of them, assign every customer to one, and trade the fixed cost of opening against the cost of serving from further away.
GenX piecewise fuel A day of dispatch for two carbon-capture plants and a wind farm under a net-zero carbon cap, where the gas plant's fuel use bends with its output.
Routing telephone calls How many of 425 requested circuits a five-city network can carry at once — and by which routes.
Choosing the mode of transport Moving 180 tonnes of chemicals out of four depots, where a depot may reach a centre by rail or by road at different cost.
OSeMOSYS UTOPIA What to build and how hard to run it, 1990–2010, to meet three end-use demands at least discounted cost.
Travelling salesman Visit every city once and come home, as cheaply as possible. The most famous problem in combinatorial optimisation, and the one most often assumed to be out of reach here.

Everything on this page is generated — the catalogue off the site nav and each page's own opening line, the constructs matrix off each model's resolved plan, the reference table off examples/ports/references.json, which is the same file the tests assert against. Regenerate with uv run python -m tools.constructs; a test fails if any of the three is stale.

Every page also carries the model as math, typeset from the same file the engine builds (uv run python -m tools.gallery_math, likewise gated). Where a model has a symbol table in examples/symbols/, the block uses the notation that model's prose already does.

Can it say my model?

Read off the resolved plan of each model rather than its text, so it cannot drift from what the engine builds.

model verified sum sum(by=) at() shift shift(edge='wrap') where bounds piecewise sos MILP
dispatch ✔ 10500 ✓ · · · · ✓ ✓ · · ·
monthly_budget ✔ 9500 ✓ ✓ · · · · ✓ · · ·
multi_period ✔ 10020 ✓ · ✓ · · · ✓ · · ·
piecewise ✔ 3850 ✓ · · · · · ✓ ✓ · ·
reserves ✔ 915 ✓ ✓ ✓ · · · ✓ · · ·
seasons · · · · · ✓ · ✓ · · ·
sos · ✓ · · · · · ✓ ✓ ✓ ·
storage ✔ 5650 ✓ · · · ✓ · ✓ · · ·
transport ✔ 4400 · ✓ · · · · ✓ · · ·
walkthrough · ✓ · · · · ✓ ✓ · · ·
facility_location ✔ 932616 ✓ · · · · · ✓ · · ✓
genx_piecewise_fuel ✔ 2341.82 ✓ · · · ✓ ✓ ✓ · · ·
osemosys_utopia ✔ 29446.9 ✓ · · · · · ✓ · · ·
pypsa_ac_dc ✔ 1.8441e+07 ✓ ✓ ✓ · · · ✓ · · ·
pypsa_committable_extendable ✔ 21700 ✓ · · · · ✓ ✓ · · ✓
pypsa_cvar ✔ 35410 ✓ · · · · · ✓ · · ·
pypsa_cyclic_storage ✔ 17228.8 · ✓ · ✓ ✓ · ✓ · · ·
pypsa_energy_sum ✔ 21400 ✓ ✓ · · · ✓ ✓ · · ·
pypsa_fixed ✔ 49900 · ✓ · · · ✓ ✓ · · ·
pypsa_global_limits ✔ 127212 ✓ ✓ ✓ · · ✓ ✓ · · ·
pypsa_growth_limit ✔ 47110 ✓ ✓ ✓ ✓ · · ✓ · · ·
pypsa_kvl ✔ 17000 ✓ ✓ · · · · ✓ · · ·
pypsa_linearized_uc ✔ 5540 ✓ · · ✓ · · ✓ · · ·
pypsa_link_delay ✔ 4311.11 · ✓ · ✓ · · ✓ · · ·
pypsa_losses ✔ 24114.2 · ✓ · · · ✓ ✓ · · ·
pypsa_min_up_down ✔ 32750 ✓ · · ✓ · ✓ ✓ · · ✓
pypsa_mixed_cycling ✔ 4800 · ✓ · ✓ ✓ ✓ ✓ · · ·
pypsa_modular ✔ 56700 · ✓ · · · · ✓ · · ✓
pypsa_multi_period ✔ 85300 ✓ · ✓ · · · ✓ · · ·
pypsa_multilink ✔ 1100 ✓ ✓ · · · · ✓ · · ·
pypsa_ramp ✔ 18200 · ✓ · ✓ · · ✓ · · ·
pypsa_spill ✔ 3200 · ✓ · ✓ · ✓ ✓ · · ·
pypsa_stochastic ✔ 33940 ✓ · · · · · ✓ · · ·
pypsa_storage ✔ 15253.2 · ✓ · ✓ · ✓ ✓ · · ·
pypsa_store ✔ 7005.5 · ✓ · ✓ · ✓ ✓ · · ·
pypsa_transport ✔ 22000 · ✓ · · · · ✓ · · ·
pypsa_unit_commitment ✔ 24900 ✓ · · ✓ · ✓ ✓ · · ✓
stigler_diet ✔ 0.108662 ✓ · · · · · ✓ · · ·
telephone_routing ✔ 380 ✓ ✓ · · · · ✓ · · ✓
transport_dantzig ✔ 153.675 ✓ · · · · · ✓ · · ·
transport_modes ✔ 1715 ✓ ✓ · · · · ✓ · · ·
transport_pwl ✔ 8.78685 ✓ · · ✓ · · ✓ ✓ · ✓
tsp_mtz ✔ 2085 ✓ ✓ · · · ✓ ✓ · · ✓

No holes left. Every construct in the table has at least one model behind it whose optimum came from somebody else. The column was worth keeping precisely because it named each gap out loud before it was filled: roll / shift went first (ramp limits), then integrality (unit commitment), and piecewise last (economies of scale).

That is a floor, not a ceiling. A tick means one verified model exercises the construct — not that every shape of it is covered, and not that the constructs are exercised in combination. The table's job from here is to stay honest as the language grows, which is why it is generated rather than maintained by hand.

Does it get the right answer?

✔ means the optimum did not come from lpspec — a figure published with the model, or a reference implementation hand-written on another stack, each row's provenance saying which. Every model on this page is run by the test suite, so "there is a test" distinguishes nothing. What the badge marks is narrower, and it is the only check that can catch a shared misreading — both lanes of the implementation agreeing on a meaning the modeller did not intend, which passes every lpspec-against-lpspec test green.

Even the differential harness compares two lanes consuming the same resolved AST (hard rule 1), which is what makes them an oracle for each other and also what they cannot see. This is the net for that class, and the evidence behind the ceiling.

port optimum rtol duals reference
dispatch 10500.0 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/dispatch.py — agreement, not a published figure
facility_location 932615.75 1e-09 · published by OR-Library (Beasley) for instance cap71 of the uncapacitated warehouse location set, in the file uncapopt: http://people.brunel.ac.uk/~mastjjb/jeb/orlib/uncapinfo.html
genx_piecewise_fuel 2341.8230753008093 1e-09 · published by GenX: asserted in test/test_piecewisefuel.jl as obj_true = 2341.82308 under genx_setup UCommit=2, CO2Cap=1, ParameterScale=1, and reproduced here by running GenX itself (julia 1.12.6, HiGHS) which reports 2341.8230753008093
monthly_budget 9500.0 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/monthly_budget.py — agreement, not a published figure
multi_period 10020.0 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/multi_period.py — agreement, not a published figure
osemosys_utopia 29446.86269 1e-09 · published by OSeMOSYS: asserted in OSeMOSYS_GNU_MathProg tests/test_gnu_mathprog.py as obj = 2.944686269e+04 for tests/utopia.txt, and reproduced here by running GLPK directly (glpsol 5.0, src/osemosys.txt) — an oracle outside Python entirely
piecewise 3850.0 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/piecewise.py — agreement, not a published figure
pypsa_ac_dc 18441021.477729216 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_ac_dc.py — n.objective + n.objective_constant, the system cost
pypsa_committable_extendable 21700.0 1e-09 · pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_committable_extendable.py
pypsa_cvar 35410.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_cvar.py
pypsa_cyclic_storage 17228.77962151063 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_cyclic_storage.py
pypsa_energy_sum 21400.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_energy_sum.py
pypsa_fixed 49900.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_fixed.py
pypsa_global_limits 127211.66666666666 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_global_limits.py
pypsa_growth_limit 47110.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_growth_limit.py
pypsa_kvl 17000.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_kvl.py
pypsa_linearized_uc 5540.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_linearized_uc.py
pypsa_link_delay 4311.111111111111 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_link_delay.py
pypsa_losses 24114.237385131008 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_losses.py
pypsa_min_up_down 32750.0 1e-09 · pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_min_up_down.py
pypsa_mixed_cycling 4800.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_mixed_cycling.py
pypsa_modular 56700.0 1e-09 · pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_modular.py
pypsa_multi_period 85300.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_multi_period.py
pypsa_multilink 1100.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_multilink.py
pypsa_ramp 18200.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_ramp.py
pypsa_spill 3200.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_spill.py
pypsa_stochastic 33940.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_stochastic.py
pypsa_storage 15253.178322993519 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_storage.py
pypsa_store 7005.5025000000005 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_store.py
pypsa_transport 22000.0 1e-09 ✔ pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_transport.py
pypsa_unit_commitment 24900.0 1e-09 · pypsa 1.2.4 (its own linopy 0.9.0), via examples/ports/references/pypsa/pypsa_unit_commitment.py
reserves 915.0 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/reserves.py — agreement, not a published figure
stigler_diet 0.10866227820675685 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/stigler_diet.py — dollars per day; x365 = $39.6617/year1
storage 5650.0 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/storage.py — agreement, not a published figure
telephone_routing 380.0 1e-09 · published by Gueret, Prins, Sevaux & Heipcke, Applications of Optimization with Xpress-MP (Dash Optimization, 2002) SS12.3.3 p. 182 — "380 out of the required 425 calls are routed"; problem and data in SS12.3, pp. 180-182
transport 4400.0 1e-09 ✔ linopy 0.9.0, via examples/ports/references/linopy/transport.py — agreement, not a published figure
transport_dantzig 153.675 1e-09 ✔ published with GAMS model library #1 (trnsport), after Dantzig, Linear Programming and Extensions (1963) ch. 3.32
transport_modes 1715.0 1e-09 · published by Gueret, Prins, Sevaux & Heipcke, Applications of Optimization with Xpress-MP (Dash Optimization, 2002) SS10.2.3 p. 143 — "The minimum cost is EUR 1,715k"; problem and data in SS10.2, p. 142
transport_pwl 8.786852757777865 1e-09 · linopy 0.9.0's own add_piecewise_formulation, via examples/ports/references/linopy/transport_pwl.py; the model is GAMS model library trnspwl (Dantzig transport with economies of scale), which publishes the formulation and its discretisation but no optimal objective
tsp_mtz 2085.0 1e-09 · published by TSPLIB for instance gr17 (Groetschel, 17 cities, EXPLICIT lower-diagonal distance matrix); optimum 2085 as listed in the TSPLIB solutions file

The objective is not the only thing checked. A port with a duals tick also records the reference's shadow prices and is asserted against them — for the PyPSA models that is buses_t.marginal_price, the nodal price, which is the output this audience reads most often after the cost.

That matters because an objective is one number and hides a great deal. A dual vector is where two implementations most reliably disagree quietly: which side of a constraint the price belongs to, and what sign an inequality carries. Dantzig transport is in that set specifically because both of its constraints are inequalities pointing opposite ways. A MILP has no dual solution, and lpspec refuses to invent one — which is what the · rows are.

Adding a port is four files and five rules: CONTRIBUTING.md.

The ladder

Reproducing a full PyPSA objective means reproducing marginal and capital cost, ramp limits, storage cycling and KVL at once, and a mismatch then implicates five features instead of one. So each network is a ladder, one feature per rung, each switched off in PyPSA and reproduced here: 1 transport model ✔ · 2 ramp limits ✔ · 3 storage with state of charge ✔ · 4 cyclic boundary condition ✔ · 5 KVL ✔ · 6 a meshed AC-DC network under a CO₂ budget ✔. Rungs 1–5 are one feature at a time on a three-bus network; rung 6 is the first that puts several of them on a network somebody else designed, which is a different question — not can it say this feature but does the whole thing still read. The ladder is the six rungs. A feature that needs no network gets a one-bus model of its own instead — PyPSA components and modes above — where a mismatch implicates the one thing switched on.

A rung that matches is a row in the table above; one that cannot be said is a row in the ledger. Both are evidence, so no rung is wasted.

Rung 2 needed the instance widened before it meant anything. Rung 1's links run saturated, which fixes every generator's output exactly, so a ramp limit on that network can only make it infeasible — never change the answer. A rung that cannot bind is not evidence that it works.

Rung 6 is where a second coordinate first earns its keep. A generator sits on a bus and burns a carrier, and both maps are load-bearing — the balance groups through one, the CO₂ budget reads an emission rate back down through the other. It also carries passive lines and controllable links at once, so both branch kinds group onto the same bus dimension in one equation.

The ladder reached rung 5 without a new primitive. Rung 5 is Kirchhoff's voltage law, and it needed nothing added to the language: a cycle basis is a sparse (cycle, line) incidence parameter, and the constraint is one sum(f * cycle_incidence, over=line) == 0. A line can belong to several cycles, so the incidence cannot be a declared lookup — that is the shape finding, and it is the same "topology is data" claim the corpus started with. Computing the basis is a graph algorithm and stays in data preparation, where the ceiling puts it.

Rung 4 made the model smaller, which is the ladder paying off in the direction nobody plans for. Closing the horizon deletes rung 3's boundary equation outright: edge='wrap' is cyclic already, so the wrap onto the last snapshot is what it does unguarded, and the acyclic case is the one needing an extra clause. Two rungs written a day apart, differing by one deleted where, is a sharper statement about the language than either alone.

Ledger — what a port could not say

Feeds the roadmap, with the verdict AGENTS.md asks for: macro, primitive, or escape.

Port What could not be said Worked around by Verdict
PyPSA rung 1 a bound of -rating — PyPSA's p_min_pu = -1 shipping neg_rating as data primitive: bounds as expressions, #31. A second model asking for it
Travelling salesman subtour cuts generated lazily inside branch-and-cut, which is how every serious TSP code works MTZ, O(n²) and static refused, and correctly: a solve loop is an algorithm, not a model

min_up_time was the third row until minimum up and down times ported it: sum_back(start_up, over=snapshot, within=min_up_time), each generator's own width read off the column.

Two rows from 33 ports — a rate worth watching once the corpus has hit the ceiling a few more times.

Shapes still without a witness

Not things the language cannot say — things no outside model in the corpus has yet been found to need. Each was searched for and not found, so the row is a standing request rather than a gap in the language:

Shape Where it was looked for
one axis grouped several ways, all of them load-bearing OSeMOSYS UTOPIA declares three maps out of its timeslice, but they feed only storage constraints and the instance builds none
a chain whose coarse end carries a constraint PyPSA's ac-dc-meshed has a country per bus and nothing constrains a country; GAMSLIB alum composes three such chains and its shipped scenario switches two of them off
a group that carries its own constraint and appears nowhere else GAMSLIB mexls is exactly this, and its optimum is published only in a book with no reachable text
a partial map whose null membership moves an optimum reserves exercises it, but that model is ours and was built to
opposite-sign legs onto one dimension, plus a second hop a constraint reads an airline fleet-assignment text has it; the data and both optima are not obtainable

A model that needs one of these is worth more to this corpus than another that exercises a shape already covered.

The TSP row is the one to read, and it is narrower than it first looked. Writing DFJ's subtour rows out in full is sayable — the subsets go in as data exactly the way KVL's cycle basis does, and an 8-city instance with all 246 subsets solves to a correct tour. There are 2ⁿ of them, so it stops being practical around twenty cities, but that is a data-size wall rather than a ceiling.

What is genuinely outside is lazy generation: solve, find the violated subsets, add rows, re-solve. That is an algorithm, and this language describes models. Since lazy generation is what every serious TSP code actually does, "lpspec can express TSP" and "lpspec is a good way to solve a large TSP" are different sentences and only the first is true.

A data-dependent row count is not what rules DFJ out: the cycle basis has one and is ordinary. What the corpus is for is catching exactly that kind of mis-attribution, where a data-size wall reads as a ceiling.


  1. Laderman (1947) at the National Bureau of Standards published $39.69/year for this data, the first serious test of the simplex method. This LP's exact optimum is 0.08% under it — his rounding, not a different model — and both select the same five foods: wheat flour, liver, cabbage, spinach, navy beans. ↩

  2. examples/ports/references/linopy/transport_dantzig.py — the same LP hand-written in linopy 0.9.0, which reaches 153.675 independently. Secondary: the published figure is what verifies the port. ↩