Skip to content

The PyPSA ladder

mathspec states PyPSA in one file, grown a rung at a time. Each rung here is that file projected onto what its network builds, shown as specsolve builds it beside the PyPSA code that builds the same network, and compared with PyPSA four ways. The PyPSA parity workflow regenerates every page's sources from the pinned mathspec on each run and fails on a diff.

objective — one number, both solves · structure — the same constraint and variable names, one block each · size — the same solver rows, columns and nonzeros · duals — every constraint's dual, per row · linopy lane — the two linopy models, label for label. ✔ identical · ≠ differs, with the recorded reason · ◌ not comparable yet.

rung objective structure size duals linopy lane
Rung 1 — transport ✔ 7182.222222222221 ✔ 7 constraints · 2 variables, name for name ✔ 45 rows · ✔ 16 columns · ✔ 57 nonzeros ✔ 45 rows ✔ 9 equal · ≠ 1 recorded
Rung 2 — storage ✔ 4456.659315422355 ✔ 18 constraints · 8 variables, name for name ✔ 103 rows · ✔ 48 columns · ✔ 166 nonzeros ✔ 103 rows, 2 negated ✔ 26 equal · ≠ 1 recorded
Rung 3 — expansion ✔ 7633.908502024291 ≠ tech_capacity_expansion_limit 5 vs 1+2+2 — one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense; transmission_expansion_cost_limit 2 vs 1+1 — one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense ✔ 184 rows · ✔ 73 columns · ✔ 328 nonzeros ✔ 184 rows, 4 negated ✔ 57 equal, 2 split · ≠ 1 recorded
Rung 4 — ramps ✔ 8785.0 ✔ 9 constraints · 2 variables, name for name ✔ 64 rows · ✔ 20 columns · ✔ 92 nonzeros ✔ 64 rows, 2 negated ✔ 11 equal · ≠ 1 recorded
Rung 5 — global constraints ✔ 10282.833333333332 ≠ operational_limit 3 vs 1+1+1 — one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense; primary_energy 3 vs 1+1+1 — one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense ✔ 102 rows · ✔ 44 columns · ✔ 190 nonzeros ✔ 102 rows, 2 negated ✔ 22 equal, 2 split · ≠ 1 recorded
Rung 6 — KVL ✔ 23962.0 ≠ transmission_expansion_cost_limit 2 vs 1+1 — one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense; transmission_volume_expansion_limit 2 vs 1+1 — one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense ✔ 123 rows · ✔ 42 columns · ✔ 204 nonzeros ✔ 123 rows ✔ 19 equal, 2 split · ≠ 1 recorded
Rung 7 — commitment ✔ 7775.0 ✔ 17 constraints · 5 variables, name for name ✔ 116 rows · ✔ 44 columns · ✔ 237 nonzeros — integer model, no duals ✔ 19 equal · ≠ 4 recorded
Rung 8 — modular and big-M ✔ 15915.0 ✔ 26 constraints · 7 variables, name for name ✔ 183 rows · ✔ 80 columns · ✔ 363 nonzeros — integer model, no duals ✔ 33 equal · ≠ 1 recorded
Rung 9 — a multi-link with four output ports ✔ 11714.4 ✔ 5 constraints · 2 variables, name for name ✔ 92 rows · ✔ 32 columns · ✔ 116 nonzeros ✔ 92 rows ✔ 7 equal · ≠ 1 recorded
Rung 10 — quadratic costs ✔ 12587.437500000098 ✔ 5 constraints · 2 variables, name for name ✔ 60 rows · ✔ 24 columns · ✔ 80 nonzeros ✔ 60 rows ✔ 8 equal
Rung 11 — PyPSA's own ac_dc_meshed example, whole ✔ 18441021.477729 ≠ objective_constant 1 vs 0 — PyPSA carries a nonzero objective constant as a fixed variable of that name; the file states no constant, and the objective is compared net of it ✔ 468 rows · ≠ 188 vs 187 columns · ✔ 1007 nonzeros ✔ 468 rows ✔ 18 equal · ≠ 1 recorded
Rung 12 — linearized unit commitment ✔ 7775.0 ✔ 21 constraints · 5 variables, name for name ✔ 128 rows · ✔ 44 columns · ✔ 288 nonzeros ✔ 128 rows, 1 negated ✔ 26 equal · ≠ 1 recorded
Rung 13 — transmission losses in tangent form ✔ 10805.29588 ≠ objective_constant 1 vs 0 — PyPSA carries a nonzero objective constant as a fixed variable of that name; the file states no constant, and the objective is compared net of it ✔ 150 rows · ≠ 46 vs 45 columns · ✔ 290 nonzeros ✔ 150 rows ✔ 22 equal · ≠ 1 recorded
Rung 14 — two futures and a risk preference ✔ 9267.386667 ✔ 12 constraints · 6 variables, name for name ✔ 87 rows · ✔ 37 columns · ✔ 148 nonzeros ✔ 87 rows ◌ cannot build yet: OracleCannotBuildError
Rung 15 — two investment periods ✔ 12747.191096 ✔ 10 constraints · 3 variables, name for name ✔ 80 rows · ✔ 31 columns · ✔ 117 nonzeros ✔ 80 rows ✔ 13 equal · ≠ 1 recorded
Rung 16 — link delay ✔ 5262.5 ✔ 5 constraints · 2 variables, name for name ✔ 52 rows · ✔ 20 columns · ✔ 67 nonzeros ✔ 52 rows ✔ 7 equal · ≠ 1 recorded
Rung 17 — process ✔ 9730.0 ✔ 15 constraints · 4 variables, name for name ✔ 70 rows · ✔ 25 columns · ✔ 108 nonzeros ✔ 70 rows, 1 negated ✔ 19 equal · ≠ 1 recorded
Rung 18 — transformer ✔ 13354.401472 ≠ objective_constant 1 vs 0 — PyPSA carries a nonzero objective constant as a fixed variable of that name; the file states no constant, and the objective is compared net of it ✔ 106 rows · ≠ 39 vs 38 columns · ✔ 170 nonzeros ✔ 106 rows ✔ 21 equal · ≠ 1 recorded
Rung 19 — transmission losses in secant form ✔ 11000.926895 ≠ objective_constant 1 vs 0 — PyPSA carries a nonzero objective constant as a fixed variable of that name; the file states no constant, and the objective is compared net of it ✔ 150 rows · ≠ 46 vs 45 columns · ✔ 290 nonzeros ✔ 150 rows ✔ 20 equal · ≠ 1 recorded
Rung 20 — phase shifter ✔ 16455.0 ✔ 10 constraints · 5 variables, name for name ✔ 88 rows · ✔ 36 columns · ✔ 128 nonzeros ✔ 88 rows ✔ 15 equal · ≠ 1 recorded
Rung 21 — a carrier's growth limit binds its non-generator builds ✔ 8452.5 ✔ 18 constraints · 8 variables, name for name ✔ 74 rows · ✔ 35 columns · ✔ 134 nonzeros ✔ 74 rows, 2 negated ✔ 26 equal · ≠ 1 recorded
Rung 22 — transformer losses in tangent form ✔ 10803.477135 ≠ objective_constant 1 vs 0 — PyPSA carries a nonzero objective constant as a fixed variable of that name; the file states no constant, and the objective is compared net of it ✔ 174 rows · ≠ 46 vs 45 columns · ✔ 338 nonzeros ✔ 174 rows ✔ 35 equal · ≠ 1 recorded
Rung 23 — transformer losses in secant form ✔ 10981.999155 ≠ Line-loss_secants-neg 4 vs 8 — one segment dimension for lines and transformers, where PyPSA places secants per component — the component with fewer secants takes the zero cut, loss >= 0, at the segments it lacks (rung 23); Line-loss_secants-pos 4 vs 8 — one segment dimension for lines and transformers, where PyPSA places secants per component — the component with fewer secants takes the zero cut, loss >= 0, at the segments it lacks (rung 23); objective_constant 1 vs 0 — PyPSA carries a nonzero objective constant as a fixed variable of that name; the file states no constant, and the objective is compared net of it ≠ 142 vs 150 rows · ≠ 46 vs 45 columns · ≠ 274 vs 282 nonzeros ✔ 142 rows ✔ 25 equal · ≠ 3 recorded
Rung 24 — must stay down ✔ 9007.5 ✔ 14 constraints · 5 variables, name for name ✔ 65 rows · ✔ 28 columns · ✔ 107 nonzeros — integer model, no duals ✔ 16 equal · ≠ 4 recorded
Rung 25 — committable links ✔ 13982.0 ✔ 34 constraints · 7 variables, name for name ✔ 227 rows · ✔ 95 columns · ✔ 491 nonzeros — integer model, no duals ✔ 41 equal · ≠ 1 recorded
Rung 26 — committable processes ✔ 15865.125 ✔ 34 constraints · 8 variables, name for name ✔ 227 rows · ✔ 95 columns · ✔ 491 nonzeros — integer model, no duals ✔ 42 equal · ≠ 1 recorded
Rung 27 — modular ramps ✔ 45469.5 ✔ 41 constraints · 18 variables, name for name ✔ 161 rows · ✔ 70 columns · ✔ 354 nonzeros — integer model, no duals ✔ 59 equal · ≠ 1 recorded
Rung 28 — a start-up ramp alone ✔ 83283.0 ✔ 32 constraints · 12 variables, name for name ✔ 152 rows · ✔ 64 columns · ✔ 306 nonzeros — integer model, no duals ✔ 35 equal · ≠ 10 recorded
Rung 29 — storage per investment period ✔ 7438.461538 ✔ 15 constraints · 6 variables, name for name ✔ 212 rows · ✔ 96 columns · ✔ 356 nonzeros ✔ 212 rows, 3 negated ✔ 21 equal · ≠ 1 recorded
Rung 30 — security-constrained ✔ 22113.333333 ✔ 26 constraints · 6 variables, name for name ✔ 240 rows · ✔ 46 columns · ✔ 412 nonzeros ✔ 240 rows ✔ 32 equal · ≠ 1 recorded
Rung 32 — storage that stands in one period only ✔ 7230.486698 ✔ 13 constraints · 6 variables, name for name ✔ 92 rows · ✔ 44 columns · ✔ 143 nonzeros ✔ 92 rows, 2 negated ✔ 19 equal · ≠ 1 recorded
Rung 33 — maintenance ✔ 7367.560185 ✔ 40 constraints · 15 variables, name for name ✔ 179 rows · ✔ 103 columns · ✔ 398 nonzeros — integer model, no duals ✔ 55 equal · ≠ 1 recorded
Rung 34 — maintenance of committable units ✔ 6420.048611 ✔ 101 constraints · 30 variables, name for name ✔ 503 rows · ✔ 277 columns · ✔ 1153 nonzeros — integer model, no duals ✔ 131 equal · ≠ 1 recorded
Rung 35 — a global constraint for one investment period ✔ 4886.764706 ✔ 15 constraints · 6 variables, name for name ✔ 155 rows · ✔ 72 columns · ✔ 266 nonzeros ✔ 155 rows, 2 negated ✔ 21 equal · ≠ 1 recorded
Rung 36 — quadratic costs on a process, a storage unit and a store ✔ 19185.241281 ✔ 17 constraints · 8 variables, name for name ✔ 84 rows · ✔ 36 columns · ✔ 126 nonzeros ✔ 84 rows, 2 negated ✔ 26 equal
Rung 37 — storage dispatch pinned ✔ 6395.833333 ✔ 18 constraints · 7 variables, name for name ✔ 76 rows · ✔ 32 columns · ✔ 114 nonzeros ✔ 76 rows, 2 negated ✔ 25 equal · ≠ 1 recorded
Rung 38 — delays per investment period ✔ 12918.75 ✔ 7 constraints · 3 variables, name for name ✔ 104 rows · ✔ 40 columns · ✔ 134 nonzeros ✔ 104 rows ✔ 10 equal · ≠ 1 recorded
Rung 39 — a negative relative growth adds nothing to a carrier's growth limit ✔ 8600.0 ✔ 9 constraints · 4 variables, name for name ✔ 44 rows · ✔ 22 columns · ✔ 70 nonzeros ✔ 44 rows, 1 negated ✔ 13 equal · ≠ 1 recorded
Rung 40 — a global constraint takes its own constant and sense in each scenario ✔ 15106.666667 ≠ primary_energy 2 vs 1+1 — one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense ✔ 86 rows · ✔ 33 columns · ✔ 140 nonzeros ✔ 86 rows ✔ 13 equal, 1 split · ≠ 1 recorded
Rung 41 — a unit's cost and a link's efficiency differ by scenario ✔ 17964.0 ✔ 5 constraints · 2 variables, name for name ✔ 96 rows · ✔ 40 columns · ✔ 136 nonzeros ✔ 96 rows ✔ 7 equal · ≠ 1 recorded
Rung 42 — an extendable unit's capital cost and build cap differ by scenario ✔ 5050.0 ✔ 9 constraints · 3 variables, name for name ✔ 84 rows · ✔ 33 columns · ✔ 116 nonzeros ✔ 84 rows ✔ 12 equal · ≠ 1 recorded
Rung 43 — a component's sign turns its term in the bus balance around ✔ 2125.0 ✔ 15 constraints · 7 variables, name for name ✔ 80 rows · ✔ 36 columns · ✔ 122 nonzeros ✔ 80 rows, 2 negated ✔ 22 equal · ≠ 1 recorded
Rung 44 — linearized commitment with the rows the integer file has ✔ 7400.0 ✔ 21 constraints · 8 variables, name for name ✔ 191 rows · ✔ 100 columns · ✔ 378 nonzeros — integer model, no duals ✔ 29 equal · ≠ 1 recorded
Rung 45 — ramp limits per snapshot ✔ 10996.0 ✔ 13 constraints · 3 variables, name for name ✔ 83 rows · ✔ 28 columns · ✔ 126 nonzeros ✔ 83 rows, 3 negated ✔ 16 equal · ≠ 1 recorded
Rung 46 — the output brought in ✔ 101294.125 ✔ 32 constraints · 7 variables, name for name ✔ 200 rows · ✔ 66 columns · ✔ 387 nonzeros — integer model, no duals ✔ 36 equal · ≠ 4 recorded
Rung 47 — the relaxed file's ramps and signs ✔ 12862.5 ✔ 18 constraints · 5 variables, name for name ✔ 179 rows · ✔ 72 columns · ✔ 353 nonzeros ✔ 179 rows, 1 negated ✔ 23 equal · ≠ 1 recorded
Rung 48 — a start and a stop cost what they cost ✔ 7325.0 ✔ 10 constraints · 4 variables, name for name ✔ 72 rows · ✔ 36 columns · ✔ 118 nonzeros — integer model, no duals ✔ 11 equal · ≠ 4 recorded
Rung 49 — a carrier's growth limit without multi_investment_periods ✔ 335.0 ✔ 9 constraints · 3 variables, name for name ✔ 42 rows · ✔ 17 columns · ✔ 58 nonzeros ✔ 42 rows ✔ 12 equal · ≠ 1 recorded
Rung 50 — a load that is not active ✔ 7380.0 ✔ 5 constraints · 2 variables, name for name ✔ 32 rows · ✔ 12 columns · ✔ 40 nonzeros ✔ 32 rows ✔ 7 equal · ≠ 1 recorded
Rung 51 — a carrier's growth limit counts an asset in the first period it stands in only, not again after it retires ✔ 3432.5 ✔ 8 constraints · 2 variables, name for name ✔ 26 rows · ✔ 10 columns · ✔ 34 nonzeros ✔ 26 rows ✔ 10 equal · ≠ 1 recorded
Rung 52 — a transmission_expansion_cost_limit row holds in every scenario ✔ 15630.0 ✔ 10 constraints · 3 variables, name for name ✔ 86 rows · ✔ 33 columns · ✔ 126 nonzeros ✔ 86 rows ✔ 13 equal · ≠ 1 recorded
Rung 53 — a transmission_volume_expansion_limit row holds in every scenario under multi_investment_periods ✔ 15005.0 ✔ 8 constraints · 3 variables, name for name ✔ 70 rows · ✔ 25 columns · ✔ 102 nonzeros ✔ 70 rows ✔ 11 equal · ≠ 1 recorded
Rung 55 — a cycle of two transformers takes its Kirchhoff voltage row in every scenario ✔ 13105.0 ✔ 8 constraints · 3 variables, name for name ✔ 152 rows · ✔ 56 columns · ✔ 208 nonzeros ✔ 152 rows, 8 inside PyPSA's range ✔ 11 equal · ≠ 1 recorded
Rung 56 — a security-constrained run over scenarios copies its rows into every scenario ✔ 18205.0 ✔ 10 constraints · 3 variables, name for name ✔ 216 rows · ✔ 56 columns · ✔ 272 nonzeros ✔ 216 rows ✔ 13 equal · ≠ 1 recorded
Rung 57 — p_nom_set pins an extendable build on a network with scenarios ✔ 4800.0 ≠ Generator-p_nom_set 2 vs 1 — the file pins a build once over the generator, where PyPSA writes the same pin once per scenario; PyPSA refuses a p_nom_set that differs by scenario (rung 57) ≠ 86 vs 85 rows · ✔ 33 columns · ≠ 118 vs 117 nonzeros ✔ 86 rows, 1 inside PyPSA's range ✔ 12 equal · ≠ 2 recorded
Rung 58 — a committable unit takes its status rows in every scenario ✔ 7430.0 ✔ 13 constraints · 5 variables, name for name ✔ 126 rows · ✔ 56 columns · ✔ 206 nonzeros — integer model, no duals ✔ 15 equal · ≠ 4 recorded
Rung 60 — efficiencies per snapshot ✔ 130562.094184 ✔ 20 constraints · 8 variables, name for name ✔ 133 rows · ✔ 52 columns · ✔ 204 nonzeros ✔ 133 rows, 1 negated ✔ 28 equal · ≠ 1 recorded
Rung 61 — voltage angle limits ✔ 121834.620832 ✔ 16 constraints · 5 variables, name for name ✔ 176 rows · ✔ 60 columns · ✔ 260 nonzeros ✔ 176 rows ✔ 21 equal · ≠ 1 recorded
Rung 62 — a start-up cost per snapshot ✔ 6850.0 ✔ 10 constraints · 4 variables, name for name ✔ 60 rows · ✔ 30 columns · ✔ 98 nonzeros — integer model, no duals ✔ 11 equal · ≠ 4 recorded
Rung 63 — modular branches and storage ✔ 13927.0 ✔ 27 constraints · 17 variables, name for name ✔ 108 rows · ✔ 52 columns · ✔ 200 nonzeros — integer model, no duals ✔ 44 equal · ≠ 1 recorded
Rung 64 — commitment edges ✔ 9245.0 ✔ 29 constraints · 13 variables, name for name ✔ 205 rows · ✔ 97 columns · ✔ 361 nonzeros — integer model, no duals ✔ 33 equal · ≠ 10 recorded
Rung 66 — cycles per period ✔ 16000.0 ✔ 8 constraints · 2 variables, name for name ✔ 62 rows · ✔ 18 columns · ✔ 86 nonzeros ✔ 62 rows ✔ 10 equal · ≠ 1 recorded
Rung 67 — linearized commitment of a link and a process ✔ 47777.333333 ✔ 39 constraints · 10 variables, name for name ✔ 184 rows · ✔ 65 columns · ✔ 420 nonzeros ✔ 184 rows, 2 negated, 6 inside PyPSA's range ✔ 49 equal · ≠ 1 recorded
Rung 68 — outage factors per period ✔ 13000.0 ✔ 8 constraints · 2 variables, name for name ✔ 46 rows · ✔ 14 columns · ✔ 60 nonzeros ✔ 46 rows ✔ 10 equal · ≠ 1 recorded
Rung 69 — storage that retires before the last counted snapshot closes a primary_energy row at the last level it holds ✔ 350.0 ✔ 14 constraints · 6 variables, name for name ✔ 33 rows · ✔ 14 columns · ✔ 48 nonzeros ✔ 33 rows, 2 negated ✔ 20 equal · ≠ 1 recorded
Rung 70 — rung 14 with a risk preference of weight zero ✔ 7946.733333 ✔ 12 constraints · 6 variables, name for name ✔ 87 rows · ✔ 37 columns · ✔ 148 nonzeros ✔ 87 rows ◌ cannot build yet: OracleCannotBuildError

The four comparisons

Both sides start from one object, the network the rung's script builds. PyPSA solves it directly; specsolve solves the file attached to the tables prep.py makes of it.

column specsolve PyPSA identical means
objective result.objective n.objective + n.objective_constant equal, relative 1e-9
structure len(result.activity(block)), len(result.primal(variable)) rows and columns of n.model per name, masked labels excluded one block per PyPSA name, equal count — a split counts as a difference
size diagnostics() rows, columns, nonzeros n.model.solver_model rows, columns, nonzeros the model handed to HiGHS is the same size on both sides
duals result.dual(block) n.model.constraints[name].dual every row's dual equal, absolute 1e-6, or inside the range of duals PyPSA's model admits on that row — against the negative where the file writes the row negated; an integer model has none
linopy lane the test oracle, tests/linopy_lane n.optimize.create_model() label for label: coefficients, sense, right-hand side, bounds, integrality, objective terms

Both sides solve one object, the network the rung's script builds — PyPSA directly, specsolve through the rung's own spec attached to the tables prep.py makes of it. A difference in structure, duals or the linopy lane is allowed only with a reason in differential/pypsa/deviations.yaml; the runner fails on one recorded nowhere and on a reason no rung needs. A rung the pinned corpus gets wrong is listed in differential/pypsa/known_failing.yaml with the issue that fixes it, and stays UNEXPLAINED here until the corpus carries the fix. A rung the linopy lane cannot build yet names the blocker instead. Not compared: primals (an optimum need not be unique).

One kind of reason is checked rather than excused. Where the file states a row as PyPSA writes it negated — a storage balance with the charge on the left, a ramp written the other way about — the dual is the negative of PyPSA's, exactly, so the runner compares it against the negative at the same tolerance and the claim is under test. Those names are marked negated below; a difference surviving the negation is red.

A dual that differs is not yet a difference. Where the optimum is degenerate — a unit at its cap exactly as the load reaches it — a row has a range of optimal duals, and the two solvers may return different ones. The runner moves that row's right-hand side in PyPSA's own model by 0.001 down and up and measures the objective's slope on each side; every value between the two slopes is an optimal dual. Ours counts as equal if it lies in that range, and PyPSA's must lie there too, or the run stops. The count is shown as inside PyPSA's range.

Recorded deviations

PyPSA name (comparison) why specsolve differs on rungs
Generator-p-ramp_limit_down (duals, negated) the initial-snapshot block states −p on the left where PyPSA writes p[t] − p[t−1] ≥ −rd·p_nom — the same row negated, so its dual is the negative Rung 3 — expansion, Rung 4 — ramps, Rung 12 — linearized unit commitment, Rung 29 — storage per investment period, Rung 45 — ramp limits per snapshot, Rung 47 — the relaxed file's ramps and signs
Generator-p_nom_set (linopy lane) the file pins a build once over the generator, where PyPSA writes the same pin once per scenario; PyPSA refuses a p_nom_set that differs by scenario (rung 57) Rung 57 — p_nom_set pins an extendable build on a network with scenarios
Generator-p_nom_set (structure) the file pins a build once over the generator, where PyPSA writes the same pin once per scenario; PyPSA refuses a p_nom_set that differs by scenario (rung 57) Rung 57 — p_nom_set pins an extendable build on a network with scenarios
Generator-shut_down (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 7 — commitment, Rung 24 — must stay down, Rung 28 — a start-up ramp alone, Rung 46 — the output brought in, Rung 48 — a start and a stop cost what they cost, Rung 58 — a committable unit takes its status rows in every scenario, Rung 62 — a start-up cost per snapshot, Rung 64 — commitment edges
Generator-start_up (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 7 — commitment, Rung 24 — must stay down, Rung 28 — a start-up ramp alone, Rung 46 — the output brought in, Rung 48 — a start and a stop cost what they cost, Rung 58 — a committable unit takes its status rows in every scenario, Rung 62 — a start-up cost per snapshot, Rung 64 — commitment edges
Generator-status (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 7 — commitment, Rung 24 — must stay down, Rung 28 — a start-up ramp alone, Rung 46 — the output brought in, Rung 48 — a start and a stop cost what they cost, Rung 58 — a committable unit takes its status rows in every scenario, Rung 62 — a start-up cost per snapshot, Rung 64 — commitment edges
Line-loss_secants-neg (linopy lane) one segment dimension for lines and transformers, where PyPSA places secants per component — the component with fewer secants takes the zero cut, loss >= 0, at the segments it lacks (rung 23) Rung 23 — transformer losses in secant form
Line-loss_secants-neg (structure) one segment dimension for lines and transformers, where PyPSA places secants per component — the component with fewer secants takes the zero cut, loss >= 0, at the segments it lacks (rung 23) Rung 23 — transformer losses in secant form
Line-loss_secants-pos (linopy lane) one segment dimension for lines and transformers, where PyPSA places secants per component — the component with fewer secants takes the zero cut, loss >= 0, at the segments it lacks (rung 23) Rung 23 — transformer losses in secant form
Line-loss_secants-pos (structure) one segment dimension for lines and transformers, where PyPSA places secants per component — the component with fewer secants takes the zero cut, loss >= 0, at the segments it lacks (rung 23) Rung 23 — transformer losses in secant form
Link-p-ramp_limit_down (duals, negated) the file states the ramp as p[t-1] - p[t] <= rd * p_nom where PyPSA writes p[t] - p[t-1] >= -rd * p_nom — the same row negated, so its dual is the negative Rung 3 — expansion, Rung 4 — ramps, Rung 45 — ramp limits per snapshot, Rung 67 — linearized commitment of a link and a process
Link-shut_down (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 28 — a start-up ramp alone, Rung 64 — commitment edges
Link-start_up (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 28 — a start-up ramp alone, Rung 64 — commitment edges
Link-status (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 28 — a start-up ramp alone, Rung 64 — commitment edges
Process-p-ramp_limit_down (duals, negated) the file states the ramp as p[t-1] - p[t] <= rd * p_nom where PyPSA writes p[t] - p[t-1] >= -rd * p_nom — the same row negated, so its dual is the negative Rung 17 — process, Rung 45 — ramp limits per snapshot, Rung 67 — linearized commitment of a link and a process
Process-shut_down (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 28 — a start-up ramp alone, Rung 64 — commitment edges
Process-start_up (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 28 — a start-up ramp alone, Rung 64 — commitment edges
Process-status (linopy lane) the cap is a row here, where PyPSA bounds the variable at 1 as well — a 0-1 binary, or a share in [0, 1] under the linearized relaxation Rung 28 — a start-up ramp alone, Rung 64 — commitment edges
StorageUnit-energy_balance (duals, negated) the file states the balance with the charge on the left; PyPSA writes −soc there, the same row negated, so its dual is the negative Rung 2 — storage, Rung 3 — expansion, Rung 5 — global constraints, Rung 21 — a carrier's growth limit binds its non-generator builds, Rung 29 — storage per investment period, Rung 32 — storage that stands in one period only, Rung 35 — a global constraint for one investment period, Rung 36 — quadratic costs on a process, a storage unit and a store, Rung 37 — storage dispatch pinned, Rung 43 — a component's sign turns its term in the bus balance around, Rung 60 — efficiencies per snapshot, Rung 69 — storage that retires before the last counted snapshot closes a primary_energy row at the last level it holds
Store-energy_balance (duals, negated) the file states the balance with the level on the left; PyPSA writes −e there, the same row negated, so its dual is the negative Rung 2 — storage, Rung 3 — expansion, Rung 5 — global constraints, Rung 21 — a carrier's growth limit binds its non-generator builds, Rung 29 — storage per investment period, Rung 32 — storage that stands in one period only, Rung 35 — a global constraint for one investment period, Rung 36 — quadratic costs on a process, a storage unit and a store, Rung 37 — storage dispatch pinned, Rung 39 — a negative relative growth adds nothing to a carrier's growth limit, Rung 43 — a component's sign turns its term in the bus balance around, Rung 69 — storage that retires before the last counted snapshot closes a primary_energy row at the last level it holds
objective (linopy lane) PyPSA's objective carries its fixed objective_constant variable as a term; the file states no constant, and the objective value is compared net of it Rung 1 — transport, Rung 2 — storage, Rung 3 — expansion, Rung 4 — ramps, Rung 5 — global constraints, Rung 6 — KVL, Rung 7 — commitment, Rung 8 — modular and big-M, Rung 9 — a multi-link with four output ports, Rung 11 — PyPSA's own ac_dc_meshed example, whole, Rung 12 — linearized unit commitment, Rung 13 — transmission losses in tangent form, Rung 15 — two investment periods, Rung 16 — link delay, Rung 17 — process, Rung 18 — transformer, Rung 19 — transmission losses in secant form, Rung 20 — phase shifter, Rung 21 — a carrier's growth limit binds its non-generator builds, Rung 22 — transformer losses in tangent form, Rung 23 — transformer losses in secant form, Rung 24 — must stay down, Rung 25 — committable links, Rung 26 — committable processes, Rung 27 — modular ramps, Rung 28 — a start-up ramp alone, Rung 29 — storage per investment period, Rung 30 — security-constrained, Rung 32 — storage that stands in one period only, Rung 33 — maintenance, Rung 34 — maintenance of committable units, Rung 35 — a global constraint for one investment period, Rung 37 — storage dispatch pinned, Rung 38 — delays per investment period, Rung 39 — a negative relative growth adds nothing to a carrier's growth limit, Rung 40 — a global constraint takes its own constant and sense in each scenario, Rung 41 — a unit's cost and a link's efficiency differ by scenario, Rung 42 — an extendable unit's capital cost and build cap differ by scenario, Rung 43 — a component's sign turns its term in the bus balance around, Rung 44 — linearized commitment with the rows the integer file has, Rung 45 — ramp limits per snapshot, Rung 46 — the output brought in, Rung 47 — the relaxed file's ramps and signs, Rung 48 — a start and a stop cost what they cost, Rung 49 — a carrier's growth limit without multi_investment_periods, Rung 50 — a load that is not active, Rung 51 — a carrier's growth limit counts an asset in the first period it stands in only, not again after it retires, Rung 52 — a transmission_expansion_cost_limit row holds in every scenario, Rung 53 — a transmission_volume_expansion_limit row holds in every scenario under multi_investment_periods, Rung 55 — a cycle of two transformers takes its Kirchhoff voltage row in every scenario, Rung 56 — a security-constrained run over scenarios copies its rows into every scenario, Rung 57 — p_nom_set pins an extendable build on a network with scenarios, Rung 58 — a committable unit takes its status rows in every scenario, Rung 60 — efficiencies per snapshot, Rung 61 — voltage angle limits, Rung 62 — a start-up cost per snapshot, Rung 63 — modular branches and storage, Rung 64 — commitment edges, Rung 66 — cycles per period, Rung 67 — linearized commitment of a link and a process, Rung 68 — outage factors per period, Rung 69 — storage that retires before the last counted snapshot closes a primary_energy row at the last level it holds
objective_constant (structure) PyPSA carries a nonzero objective constant as a fixed variable of that name; the file states no constant, and the objective is compared net of it Rung 11 — PyPSA's own ac_dc_meshed example, whole, Rung 13 — transmission losses in tangent form, Rung 18 — transformer, Rung 19 — transmission losses in secant form, Rung 22 — transformer losses in tangent form, Rung 23 — transformer losses in secant form
operational_limit (structure) one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense Rung 5 — global constraints
primary_energy (structure) one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense Rung 5 — global constraints, Rung 40 — a global constraint takes its own constant and sense in each scenario
solver model (structure) what the file states otherwise than PyPSA writes it — the objective_constant variable's column and row (rungs 11, 13), the build's floor and cap once rather than once per scenario (rung 14), a fixed build's pin once rather than once per scenario (rung 57) Rung 11 — PyPSA's own ac_dc_meshed example, whole, Rung 13 — transmission losses in tangent form, Rung 18 — transformer, Rung 19 — transmission losses in secant form, Rung 22 — transformer losses in tangent form, Rung 23 — transformer losses in secant form, Rung 57 — p_nom_set pins an extendable build on a network with scenarios
tech_capacity_expansion_limit (structure) one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense Rung 3 — expansion
transmission_expansion_cost_limit (structure) one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense Rung 3 — expansion, Rung 6 — KVL
transmission_volume_expansion_limit (structure) one block per sense — ==, <=, >= — where PyPSA writes one row per labelled constraint whatever its sense Rung 6 — KVL

Not compared, deliberately: primals — an optimum need not be unique. Counted rather than compared: the rows built per block, on each rung's page, and over the whole ladder that every block is built by some rung, every mask is partially true somewhere and every parameter is fed somewhere. The runner fails on a gap not listed in differential/pypsa/untested.txt, and on a listed one a rung now closes; the rungs that would close them are mathspec#830.

Each rung's own model is the file cut to what the rung builds. The runner builds the whole file on the rung's network, cuts it to the rows and columns that build produced, and proves the cut builds the same model, coefficient for coefficient, before anything solves; every comparison above solves the cut.