docs(pypsa): a single spec covers every pypsa model class and component - #620
FabianHofmann wants to merge 48 commits into
Conversation
…period classes Fold pypsa_stochastic and pypsa_multi_period into examples/pypsa.yaml; retire the sibling pages, symbols and example files. Add the rung 14 and rung 15 sections to the gallery page, and guard the plain-run collapse to the standard model with a frozen shape fixture.
Add the Process component (a generalized multi-port converter, superset of Link) and the full-depth Transformer (a passive branch like Line, with tap ratio and a fixed phase shift in the KVL cycle) to examples/pypsa.yaml, with symbols, the regenerated gallery, rung 17 and rung 18 reference networks and oracles, and the collapse guard extended to the new standard names.
Documentation build overview
35 files changed ·
|
|
@FabianHofmann I split the quadratics out into its own file because the spec is then quadratic. But I guess one can handle this in lpspec. |
rung_19 records the secant-mode triangle solved through the pinned pypsa, objective 10840.93, the same 150 rows as the tangent rung. The two loss-cut blocks now stand for both their tangent and secant PyPSA names, matched by a gallery helper that reads every backticked name before the dash.
A phase-shifting transformer's angle becomes a per-snapshot decision where phase_shift_min < phase_shift_max, bounded by them and entering the KVL cycle sum in place of the fixed constant; a plain run keeps the shift fixed and collapses byte-for-byte. rung_20 records the phase-shifter triangle solved through the pinned pypsa, objective 16455.0.
Yes, i would target a one-file solution for now. Quadratic expression would zero out in the linear case. Let's discuss this, but a slightly interpreting lowering (going down the complexity chain) is perhaps something we can introduce. |
A transformer carries its own loss under transmission_losses, as a line does: the loss counted against its rating, its cap, its tangent or secant cuts and half of it at either bus. A plain run collapses byte-for-byte. rung_22 and rung_23 record the transformer triangle in both modes, objectives 10643.48 and 10822.00.
…s off Generator_com_status_must_stay_down mirrors the must-stay-up block over a data-prep mask, status fixed to zero; PyPSA names the row Generator-com-status-min_down_time_must_stay_up. rung_24 records a cheap unit held off for two snapshots, objective 9007.5.
Link gains the status, start-up and shut-down variables, the committable, big-M and modular p bounds, transitions, up and down times, both must-stay rules, the committed ramp rows, n_mod and its costs, mirroring Generator. rung_25 records five committable links, objective 14013.0.
…tment Process gains the same unit commitment blocks as Link, over its internal power. rung_26 restates rung 25's links as processes drawing a quarter more than they deliver, objective 15956.125.
…nd a start-up ramp alone A committable extendable modular unit takes the ordinary ramp rows against one module, not the big-M rows. A start-up or shut-down ramp alone builds the row, and a missing limit reads as the full build. rung_27 (45469.5) and rung_28 (83283.0) record both for Generator, Link and Process.
…amp restarts at a period start
…r any one listed outage
…es weighted builds
|
@FabianHofmann What is blocking for MGA exactly? |
I still have that locally. In pypsa, the MGA is a wrapping function that runs the optimized with a specific setup and a changed objective function. I am still not sure whether I want to have that in the types of yamlot file or whether this should be logic around the optimization. I think this is a non-blocker, as in the pypsa MGA we can always fallback to lowering to linopy first, exchanging the objective in runtime without relying on a MGA yaml. Of course better setups can be thought about as soon as we have the composition of math yaml files |
…per scenario (#688) * docs(pypsa): component data spans a scenario wherever pypsa reads it per scenario * docs(pypsa): two rungs show operating and first-stage data that differ by scenario
…at the full build where a limit is missing, and schedules maintenance
…he first snapshot
…apshot and from its p_init, tightens at the full build and signs its balance
… or period weight
# Conflicts: # docs/examples/pypsa_losses.md # docs/examples/pypsa_multi_period.md # docs/examples/pypsa_quadratic.md # docs/examples/pypsa_stochastic.md # examples/pypsa_losses.yaml # examples/pypsa_multi_period.yaml # examples/pypsa_quadratic.yaml # examples/pypsa_stochastic.yaml # examples/symbols/pypsa_losses.yaml # examples/symbols/pypsa_multi_period.yaml # examples/symbols/pypsa_quadratic.yaml # examples/symbols/pypsa_stochastic.yaml # mkdocs.yml
…it stands in A rung may now record a PyPSA bug: its intended objective from an oracle, and what PyPSA 1.3.0 gives instead. Rung 51 records PyPSA/PyPSA#1938.
Rungs 52 and 53 record PyPSA/PyPSA#1939, where PyPSA 1.3.0 drops both rows.
…s own delay Rung 54 records PyPSA/PyPSA#1941, where PyPSA 1.3.0 delivers the flow twice.
…lds and committable units hold in every scenario Rungs 55 to 58 record PyPSA/PyPSA#1942 and PyPSA/PyPSA#1913, where PyPSA 1.3.0 raises.
|
closing in favour of #736 |
… it, each component adding its share of a sum by name (#736) * docs(pypsa): a single file covers the standard, stochastic and multi-period classes Fold pypsa_stochastic and pypsa_multi_period into examples/pypsa.yaml; retire the sibling pages, symbols and example files. Add the rung 14 and rung 15 sections to the gallery page, and guard the plain-run collapse to the standard model with a frozen shape fixture. * docs(pypsa): the spec covers the process and transformer components Add the Process component (a generalized multi-port converter, superset of Link) and the full-depth Transformer (a passive branch like Line, with tap ratio and a fixed phase shift in the KVL cycle) to examples/pypsa.yaml, with symbols, the regenerated gallery, rung 17 and rung 18 reference networks and oracles, and the collapse guard extended to the new standard names. * docs(pypsa): the spec covers quadratic marginal cost * docs(pypsa): the spec covers transmission losses * docs(pypsa): the spec covers the secant loss mode * docs(pypsa): the secant loss mode carries a solved reference rung_19 records the secant-mode triangle solved through the pinned pypsa, objective 10840.93, the same 150 rows as the tangent rung. The two loss-cut blocks now stand for both their tangent and secant PyPSA names, matched by a gallery helper that reads every backticked name before the dash. * docs(pypsa): the spec covers the optimised transformer phase shift A phase-shifting transformer's angle becomes a per-snapshot decision where phase_shift_min < phase_shift_max, bounded by them and entering the KVL cycle sum in place of the fixed constant; a plain run keeps the shift fixed and collapses byte-for-byte. rung_20 records the phase-shifter triangle solved through the pinned pypsa, objective 16455.0. * docs(pypsa): the carrier growth limit binds every extendable component * docs(pypsa): the spec covers transformer transmission losses A transformer carries its own loss under transmission_losses, as a line does: the loss counted against its rating, its cap, its tangent or secant cuts and half of it at either bus. A plain run collapses byte-for-byte. rung_22 and rung_23 record the transformer triangle in both modes, objectives 10643.48 and 10822.00. * docs(pypsa): a committable unit serving its brought-in down time stays off Generator_com_status_must_stay_down mirrors the must-stay-up block over a data-prep mask, status fixed to zero; PyPSA names the row Generator-com-status-min_down_time_must_stay_up. rung_24 records a cheap unit held off for two snapshots, objective 9007.5. * docs(pypsa): a committable link carries the generator's unit commitment Link gains the status, start-up and shut-down variables, the committable, big-M and modular p bounds, transitions, up and down times, both must-stay rules, the committed ramp rows, n_mod and its costs, mirroring Generator. rung_25 records five committable links, objective 14013.0. * docs(pypsa): a committable process carries the generator's unit commitment Process gains the same unit commitment blocks as Link, over its internal power. rung_26 restates rung 25's links as processes drawing a quarter more than they deliver, objective 15956.125. * docs(pypsa): a ramp row follows pypsa for a modular committed build and a start-up ramp alone A committable extendable modular unit takes the ordinary ramp rows against one module, not the big-M rows. A start-up or shut-down ramp alone builds the row, and a missing limit reads as the full build. rung_27 (45469.5) and rung_28 (83283.0) record both for Generator, Link and Process. * docs(pypsa): storage cycles or reopens per investment period, and a ramp restarts at a period start * docs(pypsa): a security-constrained run limits every branch flow after any one listed outage * docs(pypsa): an mga flag caps the system cost at a budget and minimises weighted builds * docs(pypsa): mga leaves the standard spec until it can land as a patch file Reverts 5109686. The commit is kept on docs/pypsa-mga and waits for #571. * docs(pypsa): a storage built in a later period opens at its first active snapshot * docs(pypsa): a unit may be scheduled off for maintenance * docs(pypsa): the spec states what maintenance and late-opening storage assume of the data * docs(pypsa): a growth limit counts no transformer, as pypsa does * docs(pypsa): a global constraint may count one investment period, weighted by its years * docs(pypsa): a process, a storage unit and a store may carry a quadratic marginal cost * docs(pypsa): a store's power and a storage unit's dispatch and charging may each be pinned to a schedule * docs(pypsa): a link's and a process's delay applies within each investment period * docs(pypsa): a negative relative growth adds nothing to a carrier's growth limit, as pypsa clips it at zero * docs(pypsa): a tech capacity expansion limit on a network with several scenarios is refused, as pypsa does * docs(pypsa): the carrier growth rung says a transformer counts in no carrier * docs(pypsa): a global constraint takes its own constant and sense in each scenario * docs(pypsa): the stochastic rung says which component data spans a scenario * docs(pypsa): component data spans a scenario wherever pypsa reads it per scenario (#688) * docs(pypsa): component data spans a scenario wherever pypsa reads it per scenario * docs(pypsa): two rungs show operating and first-stage data that differ by scenario * docs(pypsa): a component's sign turns its term in the bus balance around * docs(pypsa): the linearized commitment file keeps a unit down, ramps at the full build where a limit is missing, and schedules maintenance * docs(pypsa): a ramp limit may change over time and lift at a snapshot * docs(pypsa): a unit that came in running ramps from its p_init into the first snapshot * docs(pypsa): the relaxed commitment file ramps every generator per snapshot and from its p_init, tightens at the full build and signs its balance * docs(pypsa): a start and a stop cost what they cost, with no snapshot or period weight * docs(pypsa): a growth limit binds only under multi_investment_periods * docs(pypsa): a load that is not active draws nothing from its bus * docs(pypsa): a security-constrained run dissipates no transmission loss * docs: the changelog lists the pypsa spec that covers every model class and component * docs(pypsa): an efficiency, a rate or a phase shift may change from snapshot to snapshot * docs: the changelog lists efficiencies per snapshot * docs(pypsa): a growth limit counts an asset only in the first period it stands in A rung may now record a PyPSA bug: its intended objective from an oracle, and what PyPSA 1.3.0 gives instead. Rung 51 records PyPSA/PyPSA#1938. * docs(pypsa): a transmission cost or volume limit holds in every scenario Rungs 52 and 53 record PyPSA/PyPSA#1939, where PyPSA 1.3.0 drops both rows. * docs(pypsa): each scenario delays a link's and a process's flow by its own delay Rung 54 records PyPSA/PyPSA#1941, where PyPSA 1.3.0 delivers the flow twice. * docs(pypsa): transformer cycles, security-constrained runs, fixed builds and committable units hold in every scenario Rungs 55 to 58 record PyPSA/PyPSA#1942 and PyPSA/PyPSA#1913, where PyPSA 1.3.0 raises. * docs(pypsa): the first-snapshot ramp refusals link the open PyPSA question * feat(language): two files that state the same spec write one text, and canonical --check fails a file that is not in it Spec.to_yaml(canonical=True) writes the normal form: sections in one order, declarations sorted by name, every expression printed from its parsed tree with the terms of a sum and the factors of a product sorted, one term per line. python -m mathspec canonical writes it; --check exits 1 for a file not in the form and --write rewrites it. Squashes #530 and #718 onto main after #721, in its words: a file states a spec. Also normalises a named expression written on one line, which the form passed through as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog link for #731 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * feat(language): a spec is composed from files that each state part of it, and patched with files that each change part of it merge composes fragments as peers: each loads on its own, owns what it declares, and reads what a sibling declares under given:, which now takes parameters, variables, named expressions and row families. A given expression's dims bound what it reads. A given: expressions: entry marked additive: true says the name is a sum other files add terms to; each contributor declares an ordinary named expression, merge sums them and keeps the marked entry. override lays patches over a base, field by field, with null as the removal. Both return a loaded Spec. Squashes #571, #690, #728 and #729 onto #731, in #721's words. Adds the tutorial 'A spec in several files', with a test that runs every step, and sorts the names under given: in the canonical form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog link for #732 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * refactor(language): a term of a sum is an ordinary named expression, and only `merge` holds it to the rules of the sum Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #734 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The reader's description wins, and the term checks stay at load Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a file adds a term to an expression another file defines, under `given:` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Walk the loaded fragments by value Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * A term names an expression of its file, and merge adds it by name `term:` on a `given: expressions:` entry now names a named expression the same file declares, and no longer takes an expression written inline. The term is then free: it takes `cases:`, a description and every rule of a named expression, and it counts as read by the math. `merge` adds the terms by name, without brackets around a name, and keeps each term as a named expression of the composed spec. A refusal for terms that land on no name names only a near miss. The typeset term line is `name = ⋯ + term`. The tutorial, the reference, the how-to, the golden model and the notation page follow. The #734 line leaves the changelog; #732 carries the design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Name the term in a term collision, and suggest only a name a term can land on Two fragments that declare one term name now hear that terms share one namespace, not the dimension-rows advice. Terms that land on no name are refused with a near miss among the definitions and readings only, since a term name is no place for a term to land; `did_you_mean` takes `listing=False` for the case where only a near miss helps. The how-to says to name each term after its component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Cut pypsa.yaml into topic fragments whose components add named terms `examples/pypsa/` holds the 24 fragments `tools/pypsa_split.py` writes from #620's `examples/pypsa.yaml`. Each component's share of a sum (the bus balance, the cycle sum, the operating cost, the carrier additions and the five global-constraint totals) is a named expression of its own, such as `Generator_injection`, and the `term:` of its `given:` entry names it. The reader of each sum describes it: `network` and `power_flow` for the two rows, `core` for the rest. `check` merges the fragments to the canonical form of the one file with its sums written as named terms, and compares that file with `pypsa.yaml` row by row, every term substituted back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog line for #736 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * fix(language): a merged spec's descriptions do not depend on the order the fragments are passed in, and a reader's fills one its owner left out (#739) * Choose a merged description by fragment name, and let a reader's fill a gap A shared dimension or relation, a reading several fragments share, and a sum built from terms took the description of whichever fragment was passed first. Each now takes the first description in the fragments' name order, as the objective already did, so the order of the arguments reaches no field. The order of the declarations still follows the order passed in, which is presentation. A folded given declaration dropped its reader's description even where the declaration it folds into had none; it now fills that gap, and yields to the owner's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog line for #739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor(language): a term prints as the definition it is, and one rule folds every reading (#738) * refactor(language): a term prints as the definition it is, and merge takes the fragments in name order once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #738 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Retitle the changelog line: the name-order rule is #739's Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(typeset): a substituted term prints its leading minus as a subtraction, and the PyPSA split has a page per fragment Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Expect the substituted sum over both dimensions, and link the changelog line to #740 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a named expression may declare the frame it is read over Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #741 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a sum other files add terms to is declared as an expression with a frame and no body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #742 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The owner of each PyPSA sum declares it with a frame and no body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The golden model substitutes a signed sum into a plus Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * docs: a file restates a shared dimension as its dtype alone, and merge carries the one description written for it (#743) * An empty sum prints as a definition with an ellipsis for its body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The owner pages of the PyPSA split print their sums with an ellipsis Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Keep main's patch case in test_composition, which #740 carried from an earlier #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQxVP5uX2V4rpvJNPhbyR2 --------- Co-authored-by: Fabian <fab.hof@gmx.de> Co-authored-by: Claude <noreply@anthropic.com>
…s0zjwf main split pypsa_stochastic into the pypsa spec and its topic files (#736, #620). The CVaR divisor change moves to examples/pypsa.yaml and its symbols; the topic files and gallery pages are regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019C9UKzzVdzrcoK3sPrnean
Note
The following content was generated by AI.
examples/pypsa.yamlbecomes the single PyPSA spec. It covers the standard, stochastic and multi-period classes, CVaR, all components, unit commitment for generators, links and processes, quadratic cost and losses. A plain run collapses to the standard model.Remaining for PyPSA feature parity
A scan of
pypsa/optimization/(v1.3.0) againstexamples/pypsa.yamlfinds these gaps in what a plainn.optimize()builds. Linearized unit commitment stays in #622. A second scan after the merge oforigin/main, in smaller slices and comparing formulas as well as names, adds the items marked second scan.Open:
method: incremental. PyPSA takes a curve onmarginal_cost(Generator, Link, Process, Store, StorageUnit),capital_cost(every extendable component, Line and Transformer included), the Generatorefficiencyinprimary_energy(global_constraints.py:333-437), Linkefficiency{port}and Processrate{port}(pypsa/data/piecewise.csv,piecewise.py).marginal_cost_storagetakes none.add_piecewise_formulation(method="auto"),piecewise.py:185):lpfor a convex cost curve with no gate,incrementalfor every efficiency or rate curve, every gated curve and every non-convex cost curve.lp, but noincremental;adjacencyandsos2reach the same optimum under row names PyPSA does not build.where:so one file holds anlpand anincrementalblock per curve, chosen by a flag from data prep. feat(language): a piecewise block states its dims and names its links, and its where reaches links that walk a relation #630 adds it; with feat(language): a piecewise where reaches a link that walks a relation, so only some converters need a curve #635, a link that walks a relation refuseslp, soincrementalmust accept a walked link.capital_costcurve besideovernight_cost.optimize_mga,optimize_mga_in_direction) as a patch file overpypsa.yaml, after feat(language): a file says what it reads, and fragments and patches compose into one model #571. The fold is ondocs/pypsa-mga(5109686, rung 31); weights on dispatch variables such asGenerator-pare still open there.pypsa_linearized_uc.yamlas a patch file. 62 of its 95 blocks copypypsa.yaml, verbatim or withoutscenarioandactive. After feat(language): a file says what it reads, and fragments and patches compose into one model #571, a patch overpypsa.yaml(relaxed status, start and stop domains, the four tightening rows) would replace the copy and also relax Link and Process commitment, as PyPSA does.efficiency{port}, Processrate{port}, StorageUnitefficiency_store/_dispatch, Transformerphase_shift, and the Generatorefficiencyinprimary_energyare read per snapshot (constraints.py:1234-1236,:2081-2082,:1657,global_constraints.py:418). A delayed port reads its coefficient at arrival (:1522); the spec applies it before the shift.constraints.py:1634-1635); the spec'sKirchhoff_Voltage_Lawhas one cycle set, which can make a multi-period model infeasible.*-n_modand the modularity rows for every nominal attribute (variables.py:363-384,constraints.py:1715-1773); the spec has them for Generator, Link and Process only.fom_costandovernight_cost. The objective readsperiodized_cost, withfom_costand the annuity ofovernight_cost(components.py:1126-1148); the transmission cost limit reads barecapital_cost(global_constraints.py:935). The spec's descriptions say neither.Buscolumnsnom_min_{carrier}/nom_max_{carrier}still build rows (global_constraints.py:115-205).consistency.py:259-281); a bidirectional Link that came in off with only a shut-down ramp (constraints.py:1094-1150), like the Generator start-up case; PyPSA's0 × inf = 0for a fixed unbounded build (constraints.py:117-122).bus0, theperiod_weight_objectivefallback, which storage counts inprimary_energyunder per-period cycling, and capacity variables PyPSA does not build foractive = False.Done:
optimize.py:415-429); the spec multiplies byperiod_weight_objective. Done: the six start and stop terms loseperiod_weight_objectiveand keepscenario_weight(optimize.py:448-452), rung 48 (7325 vs 6525).define_growth_limitreturns withoutmulti_investment_periods(global_constraints.py:219); the spec buildsCarrier_growth_limitwherevermax_growthis set. Done: theCarrier_max_growthdescription says data prep feeds no value on a single-period run (global_constraints.py:219-220), rung 49 (335 vs 2583.33).constraints.py:1537-1538); the spec has no mask. Done:Load_activeandLoad_demandinBus_nodal_balance, rung 50 (7380 vs 14730).optimize_security_constrainedcallscreate_modelwithouttransmission_lossesorlinearized_unit_commitment(abstract.py:437-441); the spec's security rows carry the loss terms, and the rung 30 text says so. Done: the 8 security rows lose the loss terms; the keyword goes to the solver through**kwargs(abstract.py:491) and PyPSA does not warn; rung 30 text fixed, optimum unchanged.pypsa_linearized_uc.yaml. The tightening rows read the ramps filled to 1 (constraints.py:313-318), ramp limits per snapshot,p_init, ramp rows for a unit that is not committable (:1073-1078), andsign. Done, rung 47. The file states its surface: generators, links and loads with a fixed build, one scenario, every asset active, only generators committable.p_init.ramp_limit_up/_downarestatic or serieson Generator, Link and Process, and a row reads the limit at its later snapshot (constraints.py:1040-1055); a unit that came in running ramps fromp_initat the first snapshot (:1091-1106). Done: rungs 45 and 46. Open: PyPSA also builds a first-snapshot row for a non-committable unit withup_time_before = 0(refused underassumptions:), and rows without variables for a unit inactive at the first snapshot.sign. PyPSA readssignonly in the bus balance (constraints.py:1428-1429,:1538). Done:Generator_sign,Load_sign,StorageUnit_sign,Store_signinBus_nodal_balance, rung 43.pypsa_linearized_uc.yaml. PyPSA builds all three underlinearized_unit_commitment(variables.py:81-88,constraints.py:622-628,:1046-1055). Done, copied frompypsa.yaml, rung 44.components/array.py:332-395). Done in docs(pypsa): component data spans a scenario wherever pypsa reads it per scenario #688: a parameter spansscenarioexactly when PyPSA reads it per scenario (160 of 237), capital cost is weighted byscenario_weight, rungs 41 and 42. Out, as PyPSA crashes or misapplies: unit commitment,p_nom_set, a per-scenario delay, and a transformer in a cycle, each with scenarios.senseandconstant(global_constraints.py:556-557,748-749,786-861). Done:GlobalConstraint_senseandGlobalConstraint_constantspan[scenario, global_constraint], rung 40. Open: a per-scenariocarrier_attributeorinvestment_period. Out, as PyPSA bugs: PyPSA drops a transmission cost limit on any stochastic network (:916), and a volume limit on a multi-period stochastic one (descriptors.py:261-263).define_growth_limitclipsmax_relative_growthat 0 (global_constraints.py:237). Done:Carrier_relative_growthgives the value where it is positive and 0 otherwise, rung 39.tech_capacity_expansion_limiton a network with scenarios (global_constraints.py:66-68). Done, as an assumption. Open: the file cannot tell one scenario from none, so it refuses only with more than one.s_set: not a gap. The component CSVs do not declares_set, butn.addstores it as a dynamic series andconstraints.py:2000-2021then buildsLine-s_set. Rungs 6 and 18 record both rows. The rows stay.constraints.py:1324-1332,multiports.py:212-219). Done:Link_output_arrivalandProcess_output_arrivalshiftby=snapshot_period, within=period, rung 38. The delay still counts whole snapshots (duration-weighted link delay is a per-snapshot resample, above shift's positional reach #299).Store-p_set, and alsoStorageUnit-p_dispatch_setandStorageUnit-p_store_set, which the scan missed (optimize.py:836-851,constraints.py:1961-2019). Done, rung 37.variables.csvflagsProcess-p,StorageUnit-p_dispatchandStore-pbesides Generator and Link (optimize.py:317-334). Done, rung 36, with PyPSA's refusal of CVaR with any quadratic cost (optimize.py:467-474) as five assumptions, andCVaR_excessandCVaR_defbuilt only whereCVaR_omega > 0, as PyPSA builds them only under a risk preference. Open: the spec cannot tell a risk preference withomega = 0from none, and declares the CVaR variables where PyPSA builds none.primary_energyandoperational_limitsum only the snapshots ofinvestment_periodwhen it is set (global_constraints.py:373-378,600-606), each weighted by its period'syears(:326,:615), which the spec also lacked with no period set. Done:GlobalConstraint_counts_snapshot,period_weight_years, the closing-level weights, and six assumptions for PyPSA's refusals, rung 35. Open: the closing level of storage inactive at the counted snapshot (PyPSA forward-fills it), and a per-scenarioinvestment_periodorconstant.maintenance,maintenance_start,maintenance_capacityandmaintenance_statusvariables, the event-count, window, start-horizon and McCormick rows ofdefine_maintenance_constraints(constraints.py:729-845), and the maintenance terms in the fixed, extendable and committable bounds of Generator, Link and Process. Done, rungs 33 and 34. Data prep supplies each window as the relation{c}_maintenance_cover, since its width follows the snapshot weightings. PyPSA'sConsistencyErrorchecks (consistency.py:1480-1560) are stated underassumptions:, with a finite module count for a fixed modular committable build, which PyPSA does not check.cyclic_state_of_charge_per_periodandstate_of_charge_initial_per_periodfor StorageUnit,e_cyclic_per_periodande_initial_per_periodfor Store (constraints.py:2106-2185,2280-2346), and ramps at period starts. Done, rung 29.constraints.py:1418,2365). Done:Transformer_loss, rungs 22 and 23.Generator-com-status-min_down_time_must_stay_up(constraints.py:622-628) holds a unit off while the down time it brought in still binds. Done:Generator_com_status_must_stay_down(rung 24), and for Link and Process with their unit commitment (rungs 25 and 26).optimize_security_constrained,abstract.py:376-491). Done, rung 30.constraints.py:2095-2097,2270-2273). The old spec dropped that row, so the storage opened at any level. Done:StorageUnit_opens_late,Store_opens_lateand the{c}_inactive_snapshotswrap, rung 32. The one unbroken run of active snapshots and the place of{c}_opens_lateare stated underassumptions:. That{c}_inactive_snapshotsequals the count of inactive snapshots is not: acountcompares only against a whole-number literal.carrier(transformers.csv), anddefine_growth_limitanddefine_tech_capacity_expansion_limitskip it (global_constraints.py:247,:82). Done:Carrier_additionsdrops its transformer term.p_nom_modand no big-M ramp rows (constraints.py:1038-1070); Done for Generator, Link and Process, rung 27.ramp_limit_start_uporramp_limit_shut_downis set (constraints.py:1049-1050), and reads a missing limit as 1. Done, rung 28.Not in
pypsa==1.3.0, so not a gap against the pin: voltage angle limits (v_ang_max,define_voltage_angle_constraints) arrive after the tag in PyPSA #1910; in 1.3.0v_ang_maxis a placeholder. The second scan read thev1.3.0-19checkout; every other item holds at the tag.Out of scope, as solve procedures:
optimize_transmission_expansion_iteratively,optimize_with_rolling_horizon,optimize_and_run_non_linear_powerflow.PyPSA bugs found on the way
Each one reproduces on PyPSA
1.3.0and onmaster. The spec column says whatexamples/pypsa.yamldoes today.global_constraints.py:276).{c}_first_activedescriptions. Rung 51: intended 3432.5, PyPSA 5185.0.transmission_expansion_cost_limitandtransmission_volume_expansion_limitare dropped silently with scenarios, and with scenarios and periods (global_constraints.py:916,descriptors.py:263).constraints.py:1269-1276).scenario. Rung 54: intended 9190.0, PyPSA 9300.0.*_nom_setcrash with scenarios (constraints.py:1654,abstract.py:445,constraints.py:1708).KeyError), 56 (security, 18205.0,ValueError), 57 (p_nom_set, 4800.0,TypeError).constraints.py:1872). Filed by someone else.KeyError.up_time_beforeand changes when another unit is committable (constraints.py:1091-1112).{c}_came_in_running_unless_committableassumptions refuse the data and cite the issue. No rung until PyPSA chooses.optimize.py:414-429). The missing snapshot weight is documented (objective.md:194). Draft question indev/.branch_outagesas a(component, name)index,optimize_security_constrainedbuilds no security rows and solves at the plain objective, without an error (rung 30 form: 11705.0).primary_energyrow withinvestment_periodis dropped under scenarios and periods (named in the #1939 draft).Decided: answer 3. The spec states the intended math, and a rung records the difference to PyPSA until the fix ships. Done in rungs 51-58: each record holds the intended objective from an oracle PyPSA solves correctly (identical scenarios, one future at a time, or one carrier per unit), and what PyPSA 1.3.0 gives.
reference.py --checkfails, naming the issue, once PyPSA matches the intended objective. The question was whether the spec should fix these bugs in its own version:1.3.0, bugs included, so every rung matches the solver.Method, gate output, alternatives
What this changes
assumptions:block (24 entries) for what maintenance and late-opening storage assume of the data. It prints under Assumptions;reference.pybinds no data to the spec, so no rung evaluates it.pypsa_stochastic.*,pypsa_multi_period.*,pypsa_quadratic.*andpypsa_losses.*into one unifiedexamples/pypsa.yaml; retires the sibling YAML, symbols and docs pages (no alias, per the alpha stream).Link. It carries an internal powerProcess_pand a signed per-portProcess_rate(bus0 withdraws, bus1 injects). It mirrors the existinglink/link_outputdeclarations. Unit commitment, maintenance, modular builds and quadratic cost are omitted, because thelinkit mirrors does not carry them.linedepth (rung 6 KVL), plus tap ratio folded into the effective reactance and a phase shift entering the KVL cycle sum, fixed or optimised (see the phase-shifter bullet).Generator_marginal_cost_quadraticandLink_marginal_cost_quadraticdefault to zero, so the two squared objective terms are inert on a plain run and the model stays linear. This keeps one file; a quadratic objective beside integer commitment is a solver-capability concern (MIQP on HiGHS), not a spec limit, so it lives in prose, not a separate file.transmission_lossesflag.Line_losscarrieswhere: transmission_losseswithabsence: zero, so a plain run keeps the lossless rows byte-for-byte: the two- 0.5 * sum(Line_loss, ...)balance terms are empty sums, andLine_s ± Line_lossagainst the rating reads as the old bound. On, the loss sits above a fan of cuts (Line_loss_slope/Line_loss_offset, one persegment, data prep), split half at each end of the line. Ported verbatim from the retiredpypsa_losses.yaml; segment picks up a clean symbol rather than reusing the cycle set's.Line-loss_secants-pos/-neg(2 rows stacked over the segments) where the tangent mode namesLine-loss_tangents-{k}-±1(one family per segment); both are the same 48 loss-cut rows, and the mode only changes what data prep puts intoLine_loss_slope/Line_loss_offset. So no new block and no mode enum (a dead option no expression reads); each block simply stands for both PyPSA names. A corrected doc note: thes_nom_max = infrefusal fires in both modes, not only the secant one.rung_19_losses_secantssolves rung 13's triangle with{'mode': 'secants'}through the pinnedpypsa==1.3.0to objective10840.93, the same 150 rows as the tangent rung 13. The coverage row forLine-loss_secants-*movessplit→ done. A small gallery helper (_names_for) lets a block declare more than one PyPSA name, the backticked tokens before the—of its description; the first stays the section heading.phase_shift_min < phase_shift_max(a flagTransformer_phase_shift_varyingin data prep, since the language cannot compare two parameters in awhere), the shift becomes a per-snapshot decisionTransformer_phase_shift, bounded by min/max in degrees, entering the KVL cycle sum through a per-degree cycle weight. It carriesabsence: zero, and the fixed weight is zeroed for varying transformers, so a plain run keeps the shift fixed and the KVL byte-identical.rung_20_phase_shifterrecords a meshed triangle whose phase shifter reroutes cheap power around a binding leg, solved to objective16455.0.rung_17_process,rung_18_transformer) against the pinnedpypsa==1.3.0. rung 18 is a meshed triangle so the transformer flows bind under KVL; it also sets a tap ratio and a phase shift. The pre-existingrung_10_quadratic_costsandrung_13_lossesoracles are preserved and now default topypsa.yaml.Carrier_additionssumsGenerator,Link,StorageUnit,Store, andProcessbuilds by carrier, counting each in its first active period;Carrier_growth_limitreads it and its own period shift. Each of these non-generator components gains a_carrierrelation and a_first_activeparameter. Like PyPSA'sdefine_growth_limit, it counts only the components that carry acarrierattribute, so no transformer.rung_21_carrier_growthproves the six PyPSA components: a battery carrier caps a storage unit and a store at 20 in the first period, and a later store reaches 30 through relative growth, solved to objective8452.5.Line_loss:Transformer_losscarrieswhere: transmission_losses AND Transformer_activeandabsence: zero, the fourTransformer-fix/ext-s-*bounds read the loss, andTransformer_loss_upper, the loss cuts and the two half-loss balance terms follow the line blocks.r_pu_effuses the inputs_nomeven for an extendable transformer, as PyPSA does; that is data prep.rung_22_transformer_losses(tangents, objective10643.48, 174 rows) andrung_23_transformer_losses_secants(objective10822.00, 142 rows) record the same triangle in both modes, so every declared row name is built by some reference.Generator_com_status_must_stay_downholdsstatus == 0while the down time a unit brought in still binds, through the data-prep maskGenerator_must_stay_down. PyPSA names the rowGenerator-com-status-min_down_time_must_stay_up.rung_24_must_stay_downbinds it: the cheap unit stays off for two snapshots, objective9007.5, 65 rows.n_modand its modularity row, the fixed, big-M and modular p bounds, transitions, min up and down times, both must-stay rules, the committable ramp rows, and stand-by, start-up and shut-down cost. For Process the status gates the internal powerProcess_p. The blocks are per component, since a macro cannot hold a comparison orcases:.rung_25_committable_link(objective14013.0, 235 rows) andrung_26_committable_process(objective15956.125, 235 rows) bind must stay up, must stay down and min up time: removing each one lowers the objective.p_nom_mod({c}_p_nom_committed) and no big-M rows. A start-up or shut-down ramp limit alone builds a row, and a missing limit reads as 1 ({c}_ramp_up_rateand its three siblings). This also fixes rung 7'scoldunit, where the spec read a missing start-up ramp as 0; its record does not change.rung_27_modular_ramp(objective45469.5, 161 rows) andrung_28_start_up_ramp(objective83283.0, 152 rows) bind the new rows for all three components.cyclic_state_of_charge_per_periodandstate_of_charge_initial_per_periodfor StorageUnit,e_cyclic_per_periodande_initial_per_periodfor Store, as two new cases of the carried-in charge (period_cyclic,period_opening). The flags default to false and are honoured only under multi-period, as in PyPSA. The ramp rows drop every period start after the first, as PyPSA does in every multi-period run.rung_29_storage_per_periodsolves to7438.46(212 rows); with the flags off the same network solves to7815.0.outageset, empty on a plain run, relationsOutage_lineandOutage_transformer, and data-prep outage factorsLine_BODFandTransformer_BODF. Eight blocks{Line,Transformer}_{fix,ext}_s_{lower,upper}_securitycopy each flow limit, keep the monitored branch's loss term, and add the factor times the outaged branch's flow, asabstract.py:455-489does. A stochastic run with outages fails inside PyPSA 1.3.0 (abstract.py:427), so the Refusals table lists it.rung_30_security_constrainedsolves to22113.33with the outages and to17380.0without them.Why
examples/pypsa.yamlis meant to be the one file that states every PyPSA class. A dimension a run may not have (scenario, period) rode on separate files before; folding them in, with a plain run collapsing to the standard model, keeps one spec and one gallery page. Process and Transformer complete the component coverage. Quadratic cost and losses fold because a zero-default term (quadratic) and a flag-gatedabsence: zerovariable (losses) state the class without changing the plain run.Gates
pixi run test(1897 passed, after mergingorigin/mainatd74262c0),pixi run python -m tools.gallery --check(5 pages current),pixi run python -m tools.schema(no drift),pixi run docs-build(clean under--strict),pixi run compile-tex(27 documents; earlier counts of 35 included stale files in the gitignoredbuild/tex).uv run --script examples/references/pypsa/reference.py --checkreports every rung, all 46, solves to its record in the pinnedpypsa==1.3.0(viauv, the PEP 723 block; the repo itself does not depend onpypsa). This is thePyPSA referencesCI gate. The loss math is solver-proven in both modes, the phase shifter is solver-proven, and the widened growth limit is solver-proven for its six PyPSA components.pixi run lintpasses every hook except a pre-existing, environmental pyrefly error:Cannot find type stubs for module yamlinsrc/math_spec/_yaml.pyandmodel.py. This PR touches nosrc/file, so the failure is unrelated.Deliberately not done
MGA is not in this PR. It swaps the objective for every reader (
goaloversystem_cost), and its budget needs the optimum of an earlier solve. It is model-agnostic, so it lands as a patch file once feat(language): a file says what it reads, and fragments and patches compose into one model #571 composes patches.31515195reverts5109686; the fold stays ondocs/pypsa-mga.Linearized unit commitment is not folded. It needs a language change: a variable's integrality cannot depend on a flag today, so the status/start/stop variables cannot switch from integer to continuous in one file. Tracked in a variable's integrality cannot be relaxed by a flag, so linearized unit commitment needs its own file #622, a separate follow-up.