Skip to content

docs(pypsa): a single spec covers every pypsa model class and component - #620

Closed
FabianHofmann wants to merge 48 commits into
mainfrom
pypsa-complete
Closed

FabianHofmann wants to merge 48 commits into
mainfrom
pypsa-complete

Conversation

@FabianHofmann

@FabianHofmann FabianHofmann commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "Make examples/pypsa.yaml the single unified PyPSA spec covering the standard, two-stage stochastic (scenario axis) and multi-period (period axis, mask-only) classes, plus CVaR. The retired sibling files pypsa_stochastic.* and pypsa_multi_period.* are folded in. A plain run (one scenario, one all-active period, unit weights, omega=0) must collapse to the old standard model."

Prompt: "the yaml file is missing the process and the transformer component. run a sub-agent to add them"

Prompt: "run a sub-agent to integrate the quadratic cost"

Prompt: "fold losses next with a sub-agent"

Prompt: "let item 1 done sub-agent" (the secant loss mode)

Prompt: "record the secant rung in the PyPSA env; then wire it into the references and flip the coverage row to done"

Prompt: "3" (close the transformer phase-shift component gap)

Prompt: "address the widening of the the growth limit"

Prompt: "update the pull request with a to-do list of these things." (the gaps from a scan of pypsa/optimization/ against examples/pypsa.yaml)

Prompt: "1" (close the transformer-loss and must-stay-down items with a sub-agent)

Prompt: "Let's address item I3. I would argue that we want to reach parity here. So we need a must stay up and must stay down for links and processes. if that is true, trigger a sub-agent to intregrate it"

Prompt: "1" (fix the two committable ramp divergences with a sub-agent)

Prompt: "2" (update the PR title and claim)

Prompt: "Actually, all three should be a rung which we should likely include in this pr" (storage per period, security-constrained optimisation, MGA)

Prompt: "Is the MTA now in the pypsa.yaml file? And would it be perhaps better to separate it out as well as the outage security constraint optimization?" then "1" (revert MGA here and keep it on a branch as a patch file that waits for #571)

Prompt: "let's tackle f8" (storage opening in a later period)

Prompt: "Are we in sync with the latest main branch?" then "1" (merge origin/main)

Prompt: "1" (correct the piecewise to-do and mark it blocked)

Prompt: "3" (maintenance scheduling with a sub-agent)

Prompt: "this could go under an assumptions block in the yaml file right?" then "1" (state the maintenance and late-opening storage checks as assumptions)

Prompt: "2" (settle the transformer rule: the transformer counts in no carrier term, as in PyPSA)

Prompt: "1" (a fresh scan of pypsa/optimization/ against pypsa.yaml, and correct the to-do list)

Prompt: "update the tracking list on the pr first, then do 1" (close voltage angle limits, per-period global constraints, quadratic cost on Process and StorageUnit, fixed store dispatch and per-period delays, one sub-agent and rung each)

Prompt: "raise the three on pypsa: [...] and update our math-spec pr with the current bugs in pypsa so we don't lose track of them. it also should come with the question whether we should directly fix the bugs in our version"

Note

The following content was generated by AI.

examples/pypsa.yaml becomes 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) against examples/pypsa.yaml finds these gaps in what a plain n.optimize() builds. Linearized unit commitment stays in #622. A second scan after the merge of origin/main, in smaller slices and comparing formulas as well as names, adds the items marked second scan.

Open:

  • Piecewise-linear curves. Blocked on feat(language): a piecewise block states its dims and names its links, and its where reaches links that walk a relation #630 and a new method: incremental. PyPSA takes a curve on marginal_cost (Generator, Link, Process, Store, StorageUnit), capital_cost (every extendable component, Line and Transformer included), the Generator efficiency in primary_energy (global_constraints.py:333-437), Link efficiency{port} and Process rate{port} (pypsa/data/piecewise.csv, piecewise.py). marginal_cost_storage takes none.
  • MGA (optimize_mga, optimize_mga_in_direction) as a patch file over pypsa.yaml, after feat(language): a file says what it reads, and fragments and patches compose into one model #571. The fold is on docs/pypsa-mga (5109686, rung 31); weights on dispatch variables such as Generator-p are still open there.
  • Security rows without variables. In a multi-period run, where both the monitored and the outaged branch are inactive, linopy counts an empty row; the spec builds none. No rung records this case.
  • pypsa_linearized_uc.yaml as a patch file. 62 of its 95 blocks copy pypsa.yaml, verbatim or without scenario and active. After feat(language): a file says what it reads, and fragments and patches compose into one model #571, a patch over pypsa.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.
  • fourth scan Efficiencies per snapshot. Link efficiency{port}, Process rate{port}, StorageUnit efficiency_store/_dispatch, Transformer phase_shift, and the Generator efficiency in primary_energy are 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.
  • fourth scan Cycles per period. PyPSA builds the cycle matrix of each period's active branches (constraints.py:1634-1635); the spec's Kirchhoff_Voltage_Law has one cycle set, which can make a multi-period model infeasible.
  • fourth scan Modular Line, Transformer, StorageUnit and Store builds. *-n_mod and 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.
  • fourth scan fom_cost and overnight_cost. The objective reads periodized_cost, with fom_cost and the annuity of overnight_cost (components.py:1126-1148); the transmission cost limit reads bare capital_cost (global_constraints.py:935). The spec's descriptions say neither.
  • fourth scan Per-bus carrier capacity rows. The deprecated Bus columns nom_min_{carrier}/nom_max_{carrier} still build rows (global_constraints.py:115-205).
  • fourth scan Small refusals and guards. Phase-shift bounds with min > max or an infinite bound on a decided shift (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's 0 × inf = 0 for a fixed unbounded build (constraints.py:117-122).
  • fourth scan Data-prep notes. Typed lines and transformers, a branch carrier filled from bus0, the period_weight_objective fallback, which storage counts in primary_energy under per-period cycling, and capacity variables PyPSA does not build for active = False.

Done:

  • fourth scan Start-up and shut-down costs take no weight. PyPSA sums them bare, with no snapshot or period weight (optimize.py:415-429); the spec multiplies by period_weight_objective. Done: the six start and stop terms lose period_weight_objective and keep scenario_weight (optimize.py:448-452), rung 48 (7325 vs 6525).
  • fourth scan Growth limit only with periods. define_growth_limit returns without multi_investment_periods (global_constraints.py:219); the spec builds Carrier_growth_limit wherever max_growth is set. Done: the Carrier_max_growth description says data prep feeds no value on a single-period run (global_constraints.py:219-220), rung 49 (335 vs 2583.33).
  • fourth scan An inactive load. PyPSA drops an inactive load from the balance (constraints.py:1537-1538); the spec has no mask. Done: Load_active and Load_demand in Bus_nodal_balance, rung 50 (7380 vs 14730).
  • fourth scan Security runs have no losses. optimize_security_constrained calls create_model without transmission_losses or linearized_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.
  • Rest of 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), and sign. Done, rung 47. The file states its surface: generators, links and loads with a fixed build, one scenario, every asset active, only generators committable.
  • fourth scan Ramp limits per snapshot and p_init. ramp_limit_up/_down are static or series on 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 from p_init at the first snapshot (:1091-1106). Done: rungs 45 and 46. Open: PyPSA also builds a first-snapshot row for a non-committable unit with up_time_before = 0 (refused under assumptions:), and rows without variables for a unit inactive at the first snapshot.
  • second scan Component sign. PyPSA reads sign only in the bus balance (constraints.py:1428-1429, :1538). Done: Generator_sign, Load_sign, StorageUnit_sign, Store_sign in Bus_nodal_balance, rung 43.
  • Must stay down, ramps and maintenance in pypsa_linearized_uc.yaml. PyPSA builds all three under linearized_unit_commitment (variables.py:81-88, constraints.py:622-628, :1046-1055). Done, copied from pypsa.yaml, rung 44.
  • third scan Scenario-dependent component data. PyPSA reads almost every component attribute per scenario (components/array.py:332-395). Done in docs(pypsa): component data spans a scenario wherever pypsa reads it per scenario #688: a parameter spans scenario exactly when PyPSA reads it per scenario (160 of 237), capital cost is weighted by scenario_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.
  • second scan Global constraints per scenario. PyPSA builds a transmission-volume, primary-energy and operational-limit row per scenario, each with its own sense and constant (global_constraints.py:556-557, 748-749, 786-861). Done: GlobalConstraint_sense and GlobalConstraint_constant span [scenario, global_constraint], rung 40. Open: a per-scenario carrier_attribute or investment_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).
  • second scan Negative relative growth. define_growth_limit clips max_relative_growth at 0 (global_constraints.py:237). Done: Carrier_relative_growth gives the value where it is positive and 0 otherwise, rung 39.
  • second scan Refusal as an assumption. PyPSA refuses a tech_capacity_expansion_limit on 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.
  • Line and Transformer s_set: not a gap. The component CSVs do not declare s_set, but n.add stores it as a dynamic series and constraints.py:2000-2021 then builds Line-s_set. Rungs 6 and 18 record both rows. The rows stay.
  • second scan Link and Process delays per period. A port delay wraps or zero-fills inside each investment period (constraints.py:1324-1332, multiports.py:212-219). Done: Link_output_arrival and Process_output_arrival shift by=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).
  • second scan Fixed store and storage-unit dispatch. Store-p_set, and also StorageUnit-p_dispatch_set and StorageUnit-p_store_set, which the scan missed (optimize.py:836-851, constraints.py:1961-2019). Done, rung 37.
  • second scan Quadratic cost on Process, StorageUnit and Store. variables.csv flags Process-p, StorageUnit-p_dispatch and Store-p besides 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, and CVaR_excess and CVaR_def built only where CVaR_omega > 0, as PyPSA builds them only under a risk preference. Open: the spec cannot tell a risk preference with omega = 0 from none, and declares the CVaR variables where PyPSA builds none.
  • second scan A global constraint for one investment period. primary_energy and operational_limit sum only the snapshots of investment_period when it is set (global_constraints.py:373-378, 600-606), each weighted by its period's years (: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-scenario investment_period or constant.
  • Maintenance scheduling. The maintenance, maintenance_start, maintenance_capacity and maintenance_status variables, the event-count, window, start-horizon and McCormick rows of define_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's ConsistencyError checks (consistency.py:1480-1560) are stated under assumptions:, with a finite module count for a fixed modular committable build, which PyPSA does not check.
  • Storage cycling per investment period. cyclic_state_of_charge_per_period and state_of_charge_initial_per_period for StorageUnit, e_cyclic_per_period and e_initial_per_period for Store (constraints.py:2106-2185, 2280-2346), and ramps at period starts. Done, rung 29.
  • Transformer losses. PyPSA applies the loss terms and cuts to transformers too (constraints.py:1418, 2365). Done: Transformer_loss, rungs 22 and 23.
  • Must stay down. 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).
  • Security-constrained optimisation (optimize_security_constrained, abstract.py:376-491). Done, rung 30.
  • Storage opening in a later period. PyPSA opens a storage that is not per-period at its first active snapshot, and a cyclic one wraps back across the inactive snapshots (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_late and the {c}_inactive_snapshots wrap, rung 32. The one unbroken run of active snapshots and the place of {c}_opens_late are stated under assumptions:. That {c}_inactive_snapshots equals the count of inactive snapshots is not: a count compares only against a whole-number literal.
  • Decide the transformer rule. The transformer counts in no carrier term, as in PyPSA: it has no carrier (transformers.csv), and define_growth_limit and define_tech_capacity_expansion_limit skip it (global_constraints.py:247, :82). Done: Carrier_additions drops its transformer term.
  • Ramp rows of a committable, extendable, modular unit. PyPSA gives it the ordinary ramp rows sized by p_nom_mod and no big-M ramp rows (constraints.py:1038-1070); Done for Generator, Link and Process, rung 27.
  • Ramp rows from a start-up or shut-down limit alone. PyPSA builds a ramp row when only ramp_limit_start_up or ramp_limit_shut_down is 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.0 v_ang_max is a placeholder. The second scan read the v1.3.0-19 checkout; 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.0 and on master. The spec column says what examples/pypsa.yaml does today.

PyPSA issue Bug The spec today
PyPSA/PyPSA#1938 The carrier growth limit counts a retired asset again in every later period (global_constraints.py:276). Intended rule in the six {c}_first_active descriptions. Rung 51: intended 3432.5, PyPSA 5185.0.
PyPSA/PyPSA#1939 transmission_expansion_cost_limit and transmission_volume_expansion_limit are dropped silently with scenarios, and with scenarios and periods (global_constraints.py:916, descriptors.py:263). Per-scenario rows. Rung 52 (cost limit): intended 15630.0, PyPSA 11890.0. Rung 53 (volume limit, periods): intended 15005.0, PyPSA 1465.0.
PyPSA/PyPSA#1941 A link delay that differs by scenario counts the link flow twice in every scenario (constraints.py:1269-1276). Link and Process delays span scenario. Rung 54: intended 9190.0, PyPSA 9300.0.
PyPSA/PyPSA#1942 Transformer cycles, security-constrained runs and *_nom_set crash with scenarios (constraints.py:1654, abstract.py:445, constraints.py:1708). Rows stated. Rungs 55 (transformer cycle, 13105.0, PyPSA KeyError), 56 (security, 18205.0, ValueError), 57 (p_nom_set, 4800.0, TypeError).
PyPSA/PyPSA#1913 Committable units crash with scenarios (constraints.py:1872). Filed by someone else. Rows stated. Rung 58: intended 7430.0, PyPSA KeyError.
PyPSA/PyPSA#1943 The first-snapshot ramp row of a non-committable unit reads up_time_before and changes when another unit is committable (constraints.py:1091-1112). Three {c}_came_in_running_unless_committable assumptions refuse the data and cite the issue. No rung until PyPSA chooses.
not filed Start-up and shut-down costs take no investment-period weight (optimize.py:414-429). The missing snapshot weight is documented (objective.md:194). Draft question in dev/. Copies PyPSA since rung 48.
not filed With scenarios and branch_outages as a (component, name) index, optimize_security_constrained builds no security rows and solves at the plain objective, without an error (rung 30 form: 11705.0). Candidate comment on PyPSA/PyPSA#1942.
not filed A primary_energy row with investment_period is dropped under scenarios and periods (named in the #1939 draft). Not covered by a rung.

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 --check fails, naming the issue, once PyPSA matches the intended objective. The question was whether the spec should fix these bugs in its own version:

  1. Copy PyPSA 1.3.0, bugs included, so every rung matches the solver.
  2. State the intended math, and mark the PyPSA case out or refuse it with an assumption. This is what the file mostly does now.
  3. State the intended math, and add a rung that records the difference to PyPSA until the fix ships.
Method, gate output, alternatives

What this changes

  • Adds an assumptions: block (24 entries) for what maintenance and late-opening storage assume of the data. It prints under Assumptions; reference.py binds no data to the spec, so no rung evaluates it.
  • Folds pypsa_stochastic.*, pypsa_multi_period.*, pypsa_quadratic.* and pypsa_losses.* into one unified examples/pypsa.yaml; retires the sibling YAML, symbols and docs pages (no alias, per the alpha stream).
  • Adds the Process component: a generalized multi-port converter, a superset of Link. It carries an internal power Process_p and a signed per-port Process_rate (bus0 withdraws, bus1 injects). It mirrors the existing link/link_output declarations. Unit commitment, maintenance, modular builds and quadratic cost are omitted, because the link it mirrors does not carry them.
  • Adds the Transformer component at full line depth (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).
  • Adds quadratic marginal cost by additive collapse, the CVaR pattern: Generator_marginal_cost_quadratic and Link_marginal_cost_quadratic default 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.
  • Adds transmission losses behind a scalar transmission_losses flag. Line_loss carries where: transmission_losses with absence: zero, so a plain run keeps the lossless rows byte-for-byte: the two - 0.5 * sum(Line_loss, ...) balance terms are empty sums, and Line_s ± Line_loss against the rating reads as the old bound. On, the loss sits above a fan of cuts (Line_loss_slope/Line_loss_offset, one per segment, data prep), split half at each end of the line. Ported verbatim from the retired pypsa_losses.yaml; segment picks up a clean symbol rather than reusing the cycle set's.
  • Covers both loss modes, tangent and secant, from the same two blocks. PyPSA names the secant cuts Line-loss_secants-pos/-neg (2 rows stacked over the segments) where the tangent mode names Line-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 into Line_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: the s_nom_max = inf refusal fires in both modes, not only the secant one.
  • Records the secant oracle: rung_19_losses_secants solves rung 13's triangle with {'mode': 'secants'} through the pinned pypsa==1.3.0 to objective 10840.93, the same 150 rows as the tangent rung 13. The coverage row for Line-loss_secants-* moves split → 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.
  • Adds the optimised transformer phase shift. Where phase_shift_min < phase_shift_max (a flag Transformer_phase_shift_varying in data prep, since the language cannot compare two parameters in a where), the shift becomes a per-snapshot decision Transformer_phase_shift, bounded by min/max in degrees, entering the KVL cycle sum through a per-degree cycle weight. It carries absence: 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_shifter records a meshed triangle whose phase shifter reroutes cheap power around a binding leg, solved to objective 16455.0.
  • Records reference networks with oracles (rung_17_process, rung_18_transformer) against the pinned pypsa==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-existing rung_10_quadratic_costs and rung_13_losses oracles are preserved and now default to pypsa.yaml.
  • Extends the collapse guard and the frozen standard-shape fixture to the new standard names, including the zero-valued quadratic, loss and phase-shift parameters.
  • Widens the carrier growth limit from generators to every extendable component. A named expression Carrier_additions sums Generator, Link, StorageUnit, Store, and Process builds by carrier, counting each in its first active period; Carrier_growth_limit reads it and its own period shift. Each of these non-generator components gains a _carrier relation and a _first_active parameter. Like PyPSA's define_growth_limit, it counts only the components that carry a carrier attribute, so no transformer. rung_21_carrier_growth proves 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 objective 8452.5.
  • Adds transformer transmission losses, mirroring Line_loss: Transformer_loss carries where: transmission_losses AND Transformer_active and absence: zero, the four Transformer-fix/ext-s-* bounds read the loss, and Transformer_loss_upper, the loss cuts and the two half-loss balance terms follow the line blocks. r_pu_eff uses the input s_nom even for an extendable transformer, as PyPSA does; that is data prep. rung_22_transformer_losses (tangents, objective 10643.48, 174 rows) and rung_23_transformer_losses_secants (objective 10822.00, 142 rows) record the same triangle in both modes, so every declared row name is built by some reference.
  • Adds must stay down: Generator_com_status_must_stay_down holds status == 0 while the down time a unit brought in still binds, through the data-prep mask Generator_must_stay_down. PyPSA names the row Generator-com-status-min_down_time_must_stay_up. rung_24_must_stay_down binds it: the cheap unit stays off for two snapshots, objective 9007.5, 65 rows.
  • Gives committable Link and Process the full non-linearized unit commitment, block for block as for Generator, because PyPSA runs the same committable code for all three: status, start-up and shut-down variables, n_mod and 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 power Process_p. The blocks are per component, since a macro cannot hold a comparison or cases:. rung_25_committable_link (objective 14013.0, 235 rows) and rung_26_committable_process (objective 15956.125, 235 rows) bind must stay up, must stay down and min up time: removing each one lowers the objective.
  • Aligns the ramp rows with PyPSA for Generator, Link and Process. A committable, extendable, modular build gets the ordinary ramp rows sized by 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_rate and its three siblings). This also fixes rung 7's cold unit, where the spec read a missing start-up ramp as 0; its record does not change. rung_27_modular_ramp (objective 45469.5, 161 rows) and rung_28_start_up_ramp (objective 83283.0, 152 rows) bind the new rows for all three components.
  • Adds storage cycling per investment period: cyclic_state_of_charge_per_period and state_of_charge_initial_per_period for StorageUnit, e_cyclic_per_period and e_initial_per_period for 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_period solves to 7438.46 (212 rows); with the flags off the same network solves to 7815.0.
  • Adds security-constrained optimisation: an outage set, empty on a plain run, relations Outage_line and Outage_transformer, and data-prep outage factors Line_BODF and Transformer_BODF. Eight blocks {Line,Transformer}_{fix,ext}_s_{lower,upper}_security copy each flow limit, keep the monitored branch's loss term, and add the factor times the outaged branch's flow, as abstract.py:455-489 does. A stochastic run with outages fails inside PyPSA 1.3.0 (abstract.py:427), so the Refusals table lists it. rung_30_security_constrained solves to 22113.33 with the outages and to 17380.0 without them.

Why

examples/pypsa.yaml is 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-gated absence: zero variable (losses) state the class without changing the plain run.

Gates

  • Ran: pixi run test (1897 passed, after merging origin/main at d74262c0), 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 gitignored build/tex).
  • Ran the solver: uv run --script examples/references/pypsa/reference.py --check reports every rung, all 46, solves to its record in the pinned pypsa==1.3.0 (via uv, the PEP 723 block; the repo itself does not depend on pypsa). This is the PyPSA references CI 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 lint passes every hook except a pre-existing, environmental pyrefly error: Cannot find type stubs for module yaml in src/math_spec/_yaml.py and model.py. This PR touches no src/ file, so the failure is unrelated.

Deliberately not done

…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.
@FabianHofmann FabianHofmann changed the title docs(pypsa): a single spec covers all classes and the process and transformer components docs(pypsa): a single spec covers all classes, the process and transformer components, and quadratic cost Sep 22, 2026
@FBumann

FBumann commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

@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.

@FabianHofmann FabianHofmann changed the title docs(pypsa): a single spec covers all classes, the process and transformer components, and quadratic cost docs(pypsa): a single spec covers all classes, the process and transformer components, quadratic cost and losses Sep 22, 2026
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.
@FabianHofmann

Copy link
Copy Markdown
Contributor Author

@FabianHofmann I split the quadratics out into its own file because the spec is then quadratic.

But I guesser can handle this in lpspec.

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.
@FabianHofmann FabianHofmann changed the title docs(pypsa): a single spec covers all classes, the process and transformer components, quadratic cost and losses docs(pypsa): a single spec covers every pypsa class, all components and their unit commitment Sep 23, 2026
@FabianHofmann FabianHofmann changed the title docs(pypsa): a single spec covers every pypsa class, all components and their unit commitment docs(pypsa): a single spec covers every pypsa model class and component Sep 23, 2026
@FBumann

FBumann commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

@FabianHofmann What is blocking for MGA exactly?

@FabianHofmann

Copy link
Copy Markdown
Contributor Author

@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
@FBumann FBumann added the docs Documentation pages, guides, reference and README label Sep 24, 2026 — with Claude
@read-the-docs-community

read-the-docs-community Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

# 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.
@FabianHofmann

Copy link
Copy Markdown
Contributor Author

closing in favour of #736

FBumann added a commit that referenced this pull request Sep 28, 2026
… 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>
FBumann pushed a commit that referenced this pull request Sep 29, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation pages, guides, reference and README

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants