From 92cbd807bfc5e8d8fa554f57a71a0f733819cc1c Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 22 Sep 2026 13:57:32 +0200 Subject: [PATCH 01/46] 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. --- .prettierignore | 2 - docs/examples/pypsa.md | 839 ++++++++++++------ docs/examples/pypsa_multi_period.md | 372 -------- docs/examples/pypsa_stochastic.md | 365 -------- examples/pypsa.yaml | 482 ++++++---- examples/pypsa_multi_period.yaml | 229 ----- examples/pypsa_stochastic.yaml | 220 ----- .../references/pypsa/rung_14_stochastic.py | 4 +- .../references/pypsa/rung_15_multi_period.py | 3 +- examples/symbols/pypsa.yaml | 23 + examples/symbols/pypsa_multi_period.yaml | 40 - examples/symbols/pypsa_stochastic.yaml | 39 - mkdocs.yml | 2 - tests/fixtures/pypsa_standard_shape.yaml | 607 +++++++++++++ tests/test_pypsa_collapses.py | 96 ++ tools/gallery.py | 2 - 16 files changed, 1585 insertions(+), 1740 deletions(-) delete mode 100644 docs/examples/pypsa_multi_period.md delete mode 100644 docs/examples/pypsa_stochastic.md delete mode 100644 examples/pypsa_multi_period.yaml delete mode 100644 examples/pypsa_stochastic.yaml delete mode 100644 examples/symbols/pypsa_multi_period.yaml delete mode 100644 examples/symbols/pypsa_stochastic.yaml create mode 100644 tests/fixtures/pypsa_standard_shape.yaml create mode 100644 tests/test_pypsa_collapses.py diff --git a/.prettierignore b/.prettierignore index 66add10f..1aa182c8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,8 +27,6 @@ docs/examples/pypsa.md docs/examples/pypsa_quadratic.md docs/examples/pypsa_linearized_uc.md docs/examples/pypsa_losses.md -docs/examples/pypsa_stochastic.md -docs/examples/pypsa_multi_period.md # The PyPSA reference scripts write this file; prettier would reformat what # they stamp, and the two would fight over it exactly as above. diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 921b046f..9e481055 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -6,11 +6,16 @@ SPDX-License-Identifier: CC-BY-4.0 # PyPSA in one file The model a plain `n.optimize()` builds, stated as one file and grown a rung -at a time. The index below lists every row PyPSA emits (PyPSA `1.3.0`, -`pypsa/optimization/`) and links each to its block in the file. +at a time. The file also carries the two classes PyPSA switches on with a +keyword: the two-stage stochastic class over a `scenario` axis (rung 14), and +the multi-period investment class over a `period` axis (rung 15). A plain run +feeds one scenario and one all-active period, so every extra axis collapses and +the standard model returns. The index below lists every row PyPSA emits (PyPSA +`1.3.0`, `pypsa/optimization/`) and links each to its block in the file. Three rules shape the file. Bounds are the explicit rows PyPSA writes, so -their duals are row duals. Regimes are data columns and `where:` masks. Names are PyPSA's, `Component_attribute`, with a symbol table +their duals are row duals. Regimes are data columns and `where:` masks. Names +are PyPSA's, `Component_attribute`, with a symbol table (`examples/symbols/pypsa.yaml`) making the math read as math. ## Index @@ -414,7 +419,7 @@ each type is three blocks by sense. | [`transmission_expansion_cost_limit`](#transmission_expansion_cost_limit) | split | a block per sense | | [`tech_capacity_expansion_limit`](#tech_capacity_expansion_limit) | split | a block per sense | | `Bus-nom_min/max_{carrier}` | out | deprecated in PyPSA | -| [`Carrier-growth_limit`](pypsa_multi_period.md) | done | rung 15, a file of its own | +| [`Carrier-growth_limit`](#carrier-growth_limit) | done | rung 15, below | > ✔ `pypsa 1.3.0` solves this rung's network at objective `10282.833333333332`, 102 rows. @@ -1095,6 +1100,166 @@ def build(): +### Rung 14 — two-stage stochastic + +Two futures and a risk preference: `n.set_scenarios(...)` with +`n.set_risk_preference(alpha, omega)`. Everything over a snapshot spans a +scenario as well. Capacity does not, because it is chosen once before the +future is known. The operating cost is the expectation over the scenarios' +weights. A risk preference adds the CVaR (conditional value at risk) rows: an +excess per scenario and the tail's average, blended into the objective at +`omega`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Generator-p`, `Link-p`](#variable-domains) | done | over `scenario`; `Generator-p_nom` is not — chosen once | +| [`Generator-fix-p-*`, `-ext-p-*`, `Link-fix-p-*`, `Bus-nodal_balance`](#generator-fix-p-lower) | done | rungs 1 and 3, over `scenario` | +| [`CVaR-a`, `CVaR-theta`, `CVaR`](#variable-domains) | done | | +| [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension | +| [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep | +| [objective](#objective) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `9267.386666666665`, 87 rows. + +
+The network, as PyPSA code + +`rung_14_stochastic.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 14: two futures and a risk preference — capacity chosen once, dispatch per scenario.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an extendable wind unit whose availability and the south's load differ between a calm and a stormy future.""" + n = spine.build() + n.add('Generator', 'wind14', bus='south', p_nom_extendable=True, p_nom_max=100, marginal_cost=1, capital_cost=20) + n.add('Load', 'port14', bus='south') + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + n.c.loads.dynamic.p_set[('calm', 'port14')] = [10, 20, 15, 10] + n.c.loads.dynamic.p_set[('stormy', 'port14')] = [40, 60, 50, 30] + n.c.generators.dynamic.p_max_pu[('calm', 'wind14')] = [0.9, 0.7, 0.8, 0.6] + n.c.generators.dynamic.p_max_pu[('stormy', 'wind14')] = [0.3, 0.2, 0.4, 0.1] + n.set_risk_preference(alpha=0.5, omega=0.3) + return n +``` + +
+ + +### Rung 15 — investment periods + +`n.optimize(multi_investment_periods=True)`. A snapshot belongs to an +investment period. An asset stands in the periods its build year and lifetime +span. Capacity is paid once per period the asset stands in, and each period +carries a weight. A carrier may grow only so much per period. Which snapshots +an asset is active in is data prep, because a `where` reaches only the frame's +own dimensions. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Generator-p`](#variable-domains) | done | where the generator stands in the snapshot's period — `active`, data prep | +| [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | +| [`Carrier-growth_limit`](#carrier-growth_limit) | done | counted in the first period a build stands in; `edge=0` at the first period | +| [objective](#objective) | done | period weight on operation; capacity once per period it stands in | +| `StorageUnit-energy_balance` per period, ramps at period starts | out | `shift(…, by=snapshot_period, within=period)` has them; a later rung | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `12747.19109626398`, 80 rows. + +
+The network, as PyPSA code + +`rung_15_multi_period.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 15: two investment periods — build years, lifetimes, period weights and a carrier's growth limit.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: eight snapshots over two periods, a unit that retires, two wind builds capped by growth.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 2.0, 1.5, 2.5, 2.0] + n.add('Bus', ['north', 'south']) + n.add('Carrier', 'wind', max_growth=50, max_relative_growth=0.5) + n.add('Carrier', 'gas') + n.add('Generator', 'old_gas', bus='north', carrier='gas', p_nom=40, marginal_cost=30, build_year=2010, lifetime=15) + n.add( + 'Generator', + 'wind20', + bus='north', + carrier='wind', + p_nom_extendable=True, + p_nom_max=200, + marginal_cost=1, + capital_cost=100, + build_year=2020, + lifetime=30, + p_max_pu=[0.8, 0.6, 0.7, 0.5, 0.8, 0.6, 0.7, 0.5], + ) + n.add( + 'Generator', + 'wind30', + bus='south', + carrier='wind', + p_nom_extendable=True, + p_nom_max=200, + marginal_cost=1, + capital_cost=80, + build_year=2030, + lifetime=30, + p_max_pu=[0.9, 0.7, 0.6, 0.8, 0.9, 0.7, 0.6, 0.8], + ) + n.add( + 'Generator', + 'gas30', + bus='south', + carrier='gas', + p_nom_extendable=True, + p_nom_max=200, + marginal_cost=40, + capital_cost=50, + build_year=2030, + lifetime=30, + ) + n.add('Link', 'wire15', bus0='north', bus1='south', p_nom=60, p_min_pu=-1, efficiency=0.95) + n.add('Load', 'town15', bus='north', p_set=[20, 30, 25, 20, 35, 45, 40, 30]) + n.add('Load', 'port15', bus='south', p_set=[10, 20, 15, 10, 30, 40, 35, 25]) + return n +``` + +
+ + ### Rung 16 — link delay A source feeding two sinks over links whose energy arrives late. PyPSA's @@ -1183,7 +1348,6 @@ def build(): | PyPSA | status | note | | ------------------------------ | ------ | ------------------------------------ | | [`{c}-loss*`](pypsa_losses.md) | done | rung 13, a file of its own; tangent form | -| [`CVaR-*`](pypsa_stochastic.md) | done | rung 14, a file of its own | ## Refusals @@ -1207,15 +1371,16 @@ concatenation of the regime blocks, `p0`/`p1` derived from `Link-p`. ## The file -The model a plain `n.optimize()` builds, stated in one file. Every declaration is named `Component_attribute` after the PyPSA statement it stands for, and each constraint's description opens with the linopy name PyPSA gives that row, so the two can be read side by side. PyPSA's regimes — extendable, committable — are data columns and become `where:` masks. Bounds are the explicit rows PyPSA writes, so their duals are row duals. Parameters no PyPSA table carries verbatim are computed in data prep and say so in their description. +A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. Which snapshots an asset is active in, and a scenario's weight, are data prep. #### Sets | Symbol | Meaning | |---|---| -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | +| $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | | $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | | $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | @@ -1224,6 +1389,8 @@ The model a plain `n.optimize()` builds, stated in one file. Every declaration i | $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | | $`\mathcal{B}`$ | index $`b`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -1261,7 +1428,24 @@ The model a plain `n.optimize()` builds, stated in one file. Every declaration i | $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once | | $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the horizon's end — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at the first snapshots is lost | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | -| $`\mathrm{load}`$ | `Load_p_set` over $`\mathcal{T} \times \mathcal{D}`$ — demand | +| $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | +| $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | +| $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | +| $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor | +| $`\mathrm{w}^{y}`$ | `period_weight_objective` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.objective` — what a period's cost weighs | +| $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep | +| $`\mathrm{on}^{f}`$ | `Link_active` over $`\mathcal{T} \times \mathcal{L}`$ — whether a link stands in a snapshot's period — PyPSA's `active`, data prep | +| $`\mathrm{on}^{h}`$ | `StorageUnit_active` over $`\mathcal{T} \times \mathcal{S}`$ — whether a storage unit stands in a snapshot's period — PyPSA's `active`, data prep | +| $`\mathrm{on}^{e}`$ | `Store_active` over $`\mathcal{T} \times \mathcal{V}`$ — whether a store stands in a snapshot's period — PyPSA's `active`, data prep | +| $`\mathrm{on}^{s}`$ | `Line_active` over $`\mathcal{T} \times \mathcal{K}`$ — whether a line stands in a snapshot's period — PyPSA's `active`, data prep | +| $`\mathrm{W}`$ | `Generator_capital_weight` over $`\mathcal{G}`$ — the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep | +| $`\mathrm{W}^{f}`$ | `Link_capital_weight` over $`\mathcal{L}`$ — the sum of period weights a link stands in — PyPSA's `active * period_weighting`, summed, data prep | +| $`\mathrm{W}^{h}`$ | `StorageUnit_capital_weight` over $`\mathcal{S}`$ — the sum of period weights a storage unit stands in — PyPSA's `active * period_weighting`, summed, data prep | +| $`\mathrm{W}^{e}`$ | `Store_capital_weight` over $`\mathcal{V}`$ — the sum of period weights a store stands in — PyPSA's `active * period_weighting`, summed, data prep | +| $`\mathrm{W}^{s}`$ | `Line_capital_weight` over $`\mathcal{K}`$ — the sum of period weights a line stands in — PyPSA's `active * period_weighting`, summed, data prep | +| $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit | +| $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top | | $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | | $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\mathcal{T} \times \mathcal{L}`$ — a given flow schedule; a link without one has no row here | | $`\mathrm{w}^{\mathrm{sto}}`$ | `snapshot_weightings_stores` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.stores` — hours a snapshot stands for in a storage balance | @@ -1343,43 +1527,47 @@ The model a plain `n.optimize()` builds, stated in one file. Every declaration i | Symbol | Meaning | |---|---| -| $`p`$ | `Generator_p` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-p` — output of a generator in a snapshot | -| $`f`$ | `Link_p` over $`\mathcal{T} \times \mathcal{L}`$ — `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to | -| $`h^{+}`$ | `StorageUnit_p_dispatch` over $`\mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_dispatch` — power delivered to the bus | -| $`h^{-}`$ | `StorageUnit_p_store` over $`\mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_store` — power drawn from the bus into charge | -| $`\mathit{soc}`$ | `StorageUnit_state_of_charge` over $`\mathcal{T} \times \mathcal{S}`$ — `StorageUnit-state_of_charge` — energy held at the end of a snapshot | -| $`\mathit{spill}`$ | `StorageUnit_spill` over $`\mathcal{T} \times \mathcal{S}`$ — `StorageUnit-spill` — inflow passed on unused. Zero where there is no inflow, so the balance keeps its row there; the bounds are PyPSA's, on the variable rather than as rows | -| $`e`$ | `Store_e` over $`\mathcal{T} \times \mathcal{V}`$ — `Store-e` — energy held at the end of a snapshot | -| $`q`$ | `Store_p` over $`\mathcal{T} \times \mathcal{V}`$ — `Store-p` — power delivered to the bus; charging is negative | +| $`p`$ | `Generator_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-p` — output of a generator in a snapshot | +| $`f`$ | `Link_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to | +| $`h^{+}`$ | `StorageUnit_p_dispatch` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_dispatch` — power delivered to the bus | +| $`h^{-}`$ | `StorageUnit_p_store` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_store` — power drawn from the bus into charge | +| $`\mathit{soc}`$ | `StorageUnit_state_of_charge` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-state_of_charge` — energy held at the end of a snapshot | +| $`\mathit{spill}`$ | `StorageUnit_spill` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-spill` — inflow passed on unused. Zero where there is no inflow, so the balance keeps its row there; the bounds are PyPSA's, on the variable rather than as rows | +| $`e`$ | `Store_e` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — `Store-e` — energy held at the end of a snapshot | +| $`q`$ | `Store_p` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — `Store-p` — power delivered to the bus; charging is negative | | $`N`$ | `Generator_n_mod` over $`\mathcal{G}`$ — `Generator-n_mod` — how many modules of an extendable modular build | -| $`u`$ | `Generator_status` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-status` — how much of a committable unit is on: an integer the rows below cap at one, or at the module count where the build is modular | -| $`\mathit{up}`$ | `Generator_start_up` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | -| $`\mathit{dn}`$ | `Generator_shut_down` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | -| $`s`$ | `Line_s` over $`\mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | +| $`u`$ | `Generator_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-status` — how much of a committable unit is on: an integer the rows below cap at one, or at the module count where the build is modular | +| $`\mathit{up}`$ | `Generator_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | +| $`\mathit{dn}`$ | `Generator_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | +| $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`F`$ | `Link_p_nom_ext` over $`\mathcal{L}`$ — `Link-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`H`$ | `StorageUnit_p_nom_ext` over $`\mathcal{S}`$ — `StorageUnit-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`E`$ | `Store_e_nom_ext` over $`\mathcal{V}`$ — `Store-e_nom` — nominal capacity where it is a decision; the parameter of the same PyPSA name carries the fixed regime | +| $`a`$ | `CVaR_a` over $`\Xi`$ — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not | +| $`\theta`$ | `CVaR_theta` (scalar) — `CVaR-theta` — where the tail starts, the value at risk | +| $`CVaR`$ | `CVaR` (scalar) — `CVaR` — the tail's average cost, what the objective prices at `omega` | #### Definitions | Symbol | Meaning | |---|---| -| $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | -| $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | +| $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | +| $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | | $`\widetilde{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_effective` over $`\mathcal{G}`$ — the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise | -| $`\Delta^{+}`$ | `Generator_ramp_up_allowance` over $`\mathcal{T} \times \mathcal{G}`$ — how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | -| $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | +| $`\Delta^{+}`$ | `Generator_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | +| $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | -| $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | -| $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | -| $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | -| $`\mathit{primary\_energy}`$ | `primary_energy` over $`\mathcal{B}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | -| $`\mathit{operational\_limit}`$ | `operational_limit` over $`\mathcal{B}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | +| $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | +| $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | +| $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | +| $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{B}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | +| $`\mathit{operational\_limit}`$ | `operational_limit` over $`\Xi \times \mathcal{B}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | | $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\mathcal{B}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | | $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\mathcal{B}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{B}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | +| $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | $`t \ominus k`$ denotes cyclic translation: index $`t-k`$ taken modulo the size of the dimension (`roll`). Plain $`t-k`$ (`shift`) has no wraparound — terms translated past the edge are simply absent. @@ -1392,27 +1580,19 @@ $`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own ord ```yaml objective: sense: minimize - description: operating cost, each snapshot weighted by the hours it stands for + description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective) - + sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective) - + sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective) - + sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective) - + sum(Store_p * Store_marginal_cost * snapshot_weightings_objective) - + sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective) - + sum(Generator_p_nom_ext * Generator_capital_cost) - + sum(Link_p_nom_ext * Link_capital_cost) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost) - + sum(Store_e_nom_ext * Store_capital_cost) - + sum(Line_s_nom_ext * Line_capital_cost) - + sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective) - + sum(Generator_start_up * Generator_start_up_cost) - + sum(Generator_shut_down * Generator_shut_down_cost) + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + CVaR_omega * CVaR ``` ```math -\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ l \in \mathcal{L}} f_{t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ s \in \mathcal{S}} h^{+}_{t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ s \in \mathcal{S}} \mathit{soc}_{t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ s \in \mathcal{S}} \mathit{spill}_{t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ v \in \mathcal{V}} q_{t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ v \in \mathcal{V}} e_{t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} + \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} u_{t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{up}_{t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{dn}_{t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} +\min \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR ``` ### `Generator-fix-p-lower` @@ -1422,13 +1602,13 @@ objective: ```yaml Generator_fix_p_lower: description: "`Generator-fix-p-lower` — a fixed generator outputs at least its minimum" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom ``` ```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-fix-p-upper` @@ -1438,13 +1618,13 @@ p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qq ```yaml Generator_fix_p_upper: description: "`Generator-fix-p-upper` — a fixed generator outputs at most what is available" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom ``` ```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Link-fix-p-lower` @@ -1454,13 +1634,13 @@ p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qqu ```yaml Link_fix_p_lower: description: "`Link-fix-p-lower` — a fixed link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - where: not Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: not Link_p_nom_extendable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom ``` ```math -f_{t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-fix-p-upper` @@ -1470,13 +1650,13 @@ f_{t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qq ```yaml Link_fix_p_upper: description: "`Link-fix-p-upper` — a fixed link carries at most its nominal power" - dims: [snapshot, link] - where: not Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: not Link_p_nom_extendable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom ``` ```math -f_{t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Generator-ext-p-lower` @@ -1486,13 +1666,13 @@ f_{t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qqu ```yaml Generator_ext_p_lower: description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext ``` ```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-ext-p-upper` @@ -1502,13 +1682,13 @@ p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, t \in \mat ```yaml Generator_ext_p_upper: description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext ``` ```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-ext-p_nom-lower` @@ -1566,13 +1746,13 @@ P_{g} = \mathrm{p}^{\mathrm{nom,set}}_{g} \qquad \forall\, g \in \mathcal{G} \,: ```yaml Generator_e_sum_min: description: "`Generator-e_sum_min` — energy over the horizon is at least its floor; a floor of minus infinity is no row" - dims: [generator] + dims: [scenario, generator] where: Generator_e_sum_min expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) >= Generator_e_sum_min ``` ```math -\sum_{t \in \mathcal{T}} p_{t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \ge \underline{\mathrm{E}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \underline{\mathrm{E}}_{g} \text{ is defined} +\sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \ge \underline{\mathrm{E}}_{g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \underline{\mathrm{E}}_{g} \text{ is defined} ``` ### `Generator-e_sum_max` @@ -1582,13 +1762,13 @@ Generator_e_sum_min: ```yaml Generator_e_sum_max: description: "`Generator-e_sum_max` — energy over the horizon is at most its budget; a budget of infinity is no row" - dims: [generator] + dims: [scenario, generator] where: Generator_e_sum_max expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) <= Generator_e_sum_max ``` ```math -\sum_{t \in \mathcal{T}} p_{t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \le \overline{\mathrm{E}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \overline{\mathrm{E}}_{g} \text{ is defined} +\sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \le \overline{\mathrm{E}}_{g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \overline{\mathrm{E}}_{g} \text{ is defined} ``` ### `Link-ext-p-lower` @@ -1598,13 +1778,13 @@ Generator_e_sum_max: ```yaml Link_ext_p_lower: description: "`Link-ext-p-lower` — an extendable link carries at least its minimum of the chosen build, negative for the other way" - dims: [snapshot, link] - where: Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: Link_p_nom_extendable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom_ext ``` ```math -f_{t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p-upper` @@ -1614,13 +1794,13 @@ f_{t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, t \in \mat ```yaml Link_ext_p_upper: description: "`Link-ext-p-upper` — an extendable link carries at most the chosen build" - dims: [snapshot, link] - where: Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: Link_p_nom_extendable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom_ext ``` ```math -f_{t,l} \le \overline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p_nom-lower` @@ -1678,13 +1858,13 @@ F_{l} = \mathrm{f}^{\mathrm{nom,set}}_{l} \qquad \forall\, l \in \mathcal{L} \,: ```yaml StorageUnit_fix_p_dispatch_lower: description: "`StorageUnit-fix-p_dispatch-lower` — dispatch is non-negative" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch >= 0 ``` ```math -h^{+}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} +h^{+}_{\xi,t,s} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-fix-p_dispatch-upper` @@ -1694,13 +1874,13 @@ h^{+}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \ ```yaml StorageUnit_fix_p_dispatch_upper: description: "`StorageUnit-fix-p_dispatch-upper` — a fixed unit dispatches at most its nominal power" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch <= StorageUnit_p_max_pu * StorageUnit_p_nom ``` ```math -h^{+}_{t,s} \le \overline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} +h^{+}_{\xi,t,s} \le \overline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-fix-p_store-lower` @@ -1710,13 +1890,13 @@ h^{+}_{t,s} \le \overline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} ```yaml StorageUnit_fix_p_store_lower: description: "`StorageUnit-fix-p_store-lower` — storing is non-negative" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store >= 0 ``` ```math -h^{-}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} +h^{-}_{\xi,t,s} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-fix-p_store-upper` @@ -1728,13 +1908,13 @@ StorageUnit_fix_p_store_upper: description: >- `StorageUnit-fix-p_store-upper` — a fixed unit stores at most its nominal power, the minimum-per-unit column carrying that cap negated - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store <= -StorageUnit_p_min_pu * StorageUnit_p_nom ``` ```math -h^{-}_{t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} +h^{-}_{\xi,t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-fix-state_of_charge-lower` @@ -1744,13 +1924,13 @@ h^{-}_{t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s ```yaml StorageUnit_fix_state_of_charge_lower: description: "`StorageUnit-fix-state_of_charge-lower` — charge is non-negative" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge >= 0 ``` ```math -\mathit{soc}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} +\mathit{soc}_{\xi,t,s} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-fix-state_of_charge-upper` @@ -1760,13 +1940,13 @@ StorageUnit_fix_state_of_charge_lower: ```yaml StorageUnit_fix_state_of_charge_upper: description: "`StorageUnit-fix-state_of_charge-upper` — a fixed unit holds at most its hours at nominal power" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge <= StorageUnit_max_hours * StorageUnit_p_nom ``` ```math -\mathit{soc}_{t,s} \le \mathrm{T}^{h}_{s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} +\mathit{soc}_{\xi,t,s} \le \mathrm{T}^{h}_{s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `Generator-com-p-lower` @@ -1776,13 +1956,13 @@ StorageUnit_fix_state_of_charge_upper: ```yaml Generator_com_p_lower: description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing" - dims: [snapshot, generator] - where: Generator_committable AND not Generator_p_nom_extendable + dims: [scenario, snapshot, generator] + where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * Generator_status ``` ```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-p-upper` @@ -1792,13 +1972,13 @@ p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cd ```yaml Generator_com_p_upper: description: "`Generator-com-p-upper` — a committed unit outputs at most what is available; off, at most nothing" - dims: [snapshot, generator] - where: Generator_committable AND not Generator_p_nom_extendable + dims: [scenario, snapshot, generator] + where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * Generator_status ``` ```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-transition-start-up` @@ -1808,13 +1988,13 @@ p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdo ```yaml Generator_com_transition_start_up: description: "`Generator-com-transition-start-up` — turning on is a start, counted against the state the unit carried into the snapshot" - dims: [snapshot, generator] - where: Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_active expression: Generator_start_up >= Generator_status - Generator_previous_status ``` ```math -\mathit{up}_{t,g} \ge u_{t,g} - \overleftarrow{u}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} +\mathit{up}_{\xi,t,g} \ge u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-transition-shut-down` @@ -1824,13 +2004,13 @@ Generator_com_transition_start_up: ```yaml Generator_com_transition_shut_down: description: "`Generator-com-transition-shut-down` — turning off is a stop, counted against the state the unit carried into the snapshot" - dims: [snapshot, generator] - where: Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_active expression: Generator_shut_down >= Generator_previous_status - Generator_status ``` ```math -\mathit{dn}_{t,g} \ge \overleftarrow{u}_{t,g} - u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} +\mathit{dn}_{\xi,t,g} \ge \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-up-time` @@ -1843,13 +2023,13 @@ Generator_com_up_time: `Generator-com-up-time` — a unit started within its own minimum up time is still on. The first snapshot's share of the window is the brought-in up time's, which the must-stay-up mask carries - dims: [snapshot, generator] - where: Generator_committable AND Generator_min_up_time > 0 AND position(snapshot) > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_min_up_time > 0 AND position(snapshot) > 0 AND Generator_active expression: sum_back(Generator_start_up, along=snapshot, window=Generator_min_up_time) <= Generator_status ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}} \mathit{up}_{t',g} \le u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{UT}_{g} > 0 \wedge \mathrm{pos}(t) > 0 +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}} \mathit{up}_{\xi,t',g} \le u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{UT}_{g} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-down-time` @@ -1859,13 +2039,13 @@ Generator_com_up_time: ```yaml Generator_com_down_time: description: "`Generator-com-down-time` — a unit stopped within its own minimum down time is still off" - dims: [snapshot, generator] - where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 AND Generator_active expression: sum_back(Generator_shut_down, along=snapshot, window=Generator_min_down_time) <= 1 - Generator_status ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}} \mathit{dn}_{t',g} \le 1 - u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{DT}_{g} > 0 \wedge \mathrm{pos}(t) > 0 +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}} \mathit{dn}_{\xi,t',g} \le 1 - u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{DT}_{g} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-status-min_up_time_must_stay_up` @@ -1875,13 +2055,13 @@ Generator_com_down_time: ```yaml Generator_com_status_must_stay_up: description: "`Generator-com-status-min_up_time_must_stay_up` — a unit still serving the up time it brought in stays on" - dims: [snapshot, generator] - where: Generator_committable AND Generator_must_stay_up + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_must_stay_up AND Generator_active expression: Generator_status == 1 ``` ```math -u_{t,g} = 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{hold}_{t,g} +u_{\xi,t,g} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{hold}_{t,g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-run-bigM` @@ -1894,10 +2074,10 @@ Generator_p_ramp_limit_up_run_big_m: `Generator-p-ramp_limit_up-run-bigM` — a committed extendable unit raises output no faster than its limit of the chosen build; the big M releases the row in the snapshot it turns on - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_ramp_limit_up * Generator_p_nom_ext @@ -1905,7 +2085,7 @@ Generator_p_ramp_limit_up_run_big_m: ``` ```math -p_{t,g} - \overleftarrow{p}_{t,g} \le \mathrm{ru}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \overleftarrow{u}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{ru}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \mathrm{ru}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{ru}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-start-bigM` @@ -1918,10 +2098,10 @@ Generator_p_ramp_limit_up_start_big_m: `Generator-p-ramp_limit_up-start-bigM` — in the snapshot it turns on, a committed extendable unit ramps no further than its start-up ramp of the chosen build; the big M releases the row everywhere else - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_ramp_limit_start_up * Generator_p_nom_ext @@ -1929,7 +2109,7 @@ Generator_p_ramp_limit_up_start_big_m: ``` ```math -p_{t,g} - \overleftarrow{p}_{t,g} \le \mathrm{ru}^{\mathrm{up}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{up}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{ru}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \mathrm{ru}^{\mathrm{up}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{ru}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-run-bigM` @@ -1942,10 +2122,10 @@ Generator_p_ramp_limit_down_run_big_m: `Generator-p-ramp_limit_down-run-bigM` — a committed extendable unit lowers output no faster than its limit of the chosen build; the big M releases the row in the snapshot it turns off - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_ramp_limit_down * Generator_p_nom_ext @@ -1953,7 +2133,7 @@ Generator_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{p}_{t,g} - p_{t,g} \le \mathrm{rd}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{rd}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \mathrm{rd}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{rd}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-shut-bigM` @@ -1966,10 +2146,10 @@ Generator_p_ramp_limit_down_shut_big_m: `Generator-p-ramp_limit_down-shut-bigM` — in the snapshot it turns off, a committed extendable unit ramps no further than its shut-down ramp of the chosen build; the big M releases the row everywhere else - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_ramp_limit_shut_down * Generator_p_nom_ext @@ -1977,7 +2157,7 @@ Generator_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{p}_{t,g} - p_{t,g} \le \mathrm{rd}^{\mathrm{dn}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{dn}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{rd}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \mathrm{rd}^{\mathrm{dn}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{rd}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p_nom_modularity` @@ -2005,13 +2185,13 @@ Generator_com_ext_p_upper_cap: description: >- `Generator-com-ext-p-upper-cap` — a committed extendable unit outputs at most what is available of the chosen build, whatever its status - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext ``` ```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-upper-bigM` @@ -2021,13 +2201,13 @@ p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, t \in \math ```yaml Generator_com_ext_p_upper_big_m: description: "`Generator-com-ext-p-upper-bigM` — off, a unit outputs nothing; on, the big M is no bound" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: Generator_p <= Generator_big_m * Generator_status ``` ```math -p_{t,g} \le \mathrm{M}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) +p_{\xi,t,g} \le \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-lower` @@ -2039,8 +2219,8 @@ Generator_com_ext_p_lower: description: >- `Generator-com-ext-p-lower` — a committed extendable unit outputs at least its minimum of the chosen build; off, the big M releases the row - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: >- Generator_p >= Generator_p_min_pu * Generator_p_nom_ext @@ -2048,7 +2228,7 @@ Generator_com_ext_p_lower: ``` ```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} + \mathrm{M}_{g} \cdot u_{t,g} - \mathrm{M}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} + \mathrm{M}_{g} \cdot u_{\xi,t,g} - \mathrm{M}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-lower-nonneg` @@ -2061,15 +2241,15 @@ Generator_com_ext_p_lower_nonneg: `Generator-com-ext-p-lower-nonneg` — where no minimum-per-unit is negative, output is also plainly non-negative, a row the big-M lower cannot assert while the unit is off - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable - AND Generator_p_min_pu_nonneg AND NOT (Generator_p_nom_mod > 0) + AND Generator_p_min_pu_nonneg AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: Generator_p >= 0 ``` ```math -p_{t,g} \ge 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{nonneg}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) +p_{\xi,t,g} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{nonneg}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-mod-p-lower` @@ -2081,13 +2261,13 @@ Generator_com_mod_p_lower: description: >- `Generator-com-mod-p-lower` — a committed modular unit outputs at least its minimum of one module, whether the build is fixed or a decision - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_mod * Generator_status ``` ```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-mod-p-upper` @@ -2099,13 +2279,13 @@ Generator_com_mod_p_upper: description: >- `Generator-com-mod-p-upper` — a committed modular unit outputs at most one module's share, whether the build is fixed or a decision - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_mod * Generator_status ``` ```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-status-p-fixed-upper` @@ -2118,13 +2298,13 @@ Generator_status_p_fixed_upper: `Generator-status-p-fixed-upper` — a status is at most the modules in place, an explicit row as PyPSA writes it: one where the build is not modular, and the fixed build's whole count of modules where it is - dims: [snapshot, generator] - where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) AND Generator_active expression: Generator_status <= Generator_modules_installed ``` ```math -u_{t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) +u_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-start_up-p-fixed-upper` @@ -2137,13 +2317,13 @@ Generator_start_up_p_fixed_upper: `Generator-start_up-p-fixed-upper` — a start is at most the modules in place, an explicit row as PyPSA writes it: one where the build is not modular, and the fixed build's whole count of modules where it is - dims: [snapshot, generator] - where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) AND Generator_active expression: Generator_start_up <= Generator_modules_installed ``` ```math -\mathit{up}_{t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) +\mathit{up}_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-shut_down-p-fixed-upper` @@ -2156,13 +2336,13 @@ Generator_shut_down_p_fixed_upper: `Generator-shut_down-p-fixed-upper` — a stop is at most the modules in place, an explicit row as PyPSA writes it: one where the build is not modular, and the fixed build's whole count of modules where it is - dims: [snapshot, generator] - where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) AND Generator_active expression: Generator_shut_down <= Generator_modules_installed ``` ```math -\mathit{dn}_{t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) +\mathit{dn}_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-status-p_nom-variable-upper` @@ -2172,13 +2352,13 @@ Generator_shut_down_p_fixed_upper: ```yaml Generator_status_p_nom_variable_upper: description: "`Generator-status-p_nom-variable-upper` — a modular unit is on only where a module is built" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_status <= Generator_n_mod ``` ```math -u_{t,g} \le N_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 +u_{\xi,t,g} \le N_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-start_up-p_nom-variable-upper` @@ -2188,13 +2368,13 @@ u_{t,g} \le N_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \ ```yaml Generator_start_up_p_nom_variable_upper: description: "`Generator-start_up-p_nom-variable-upper` — a modular unit starts only where a module is built" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_start_up <= Generator_n_mod ``` ```math -\mathit{up}_{t,g} \le N_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 +\mathit{up}_{\xi,t,g} \le N_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-shut_down-p_nom-variable-upper` @@ -2204,13 +2384,13 @@ Generator_start_up_p_nom_variable_upper: ```yaml Generator_shut_down_p_nom_variable_upper: description: "`Generator-shut_down-p_nom-variable-upper` — a modular unit stops only where a module is built" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_shut_down <= Generator_n_mod ``` ```math -\mathit{dn}_{t,g} \le N_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 +\mathit{dn}_{\xi,t,g} \le N_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Line-fix-s-lower` @@ -2220,13 +2400,13 @@ Generator_shut_down_p_nom_variable_upper: ```yaml Line_fix_s_lower: description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating" - dims: [snapshot, line] - where: not Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: not Line_s_nom_extendable AND Line_active expression: Line_s >= -Line_s_max_pu * Line_s_nom ``` ```math -s_{t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} +s_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-fix-s-upper` @@ -2236,13 +2416,13 @@ s_{t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qq ```yaml Line_fix_s_upper: description: "`Line-fix-s-upper` — a fixed line carries at most its rating" - dims: [snapshot, line] - where: not Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: not Line_s_nom_extendable AND Line_active expression: Line_s <= Line_s_max_pu * Line_s_nom ``` ```math -s_{t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} +s_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s-lower` @@ -2252,13 +2432,13 @@ s_{t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qqu ```yaml Line_ext_s_lower: description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: Line_s_nom_extendable AND Line_active expression: Line_s >= -Line_s_max_pu * Line_s_nom_ext ``` ```math -s_{t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} +s_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s-upper` @@ -2268,13 +2448,13 @@ s_{t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, t \in \mat ```yaml Line_ext_s_upper: description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: Line_s_nom_extendable AND Line_active expression: Line_s <= Line_s_max_pu * Line_s_nom_ext ``` ```math -s_{t,k} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} +s_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s_nom-lower` @@ -2332,13 +2512,13 @@ S_{k} = \mathrm{s}^{\mathrm{nom,set}}_{k} \qquad \forall\, k \in \mathcal{K} \,: ```yaml Line_s_set: description: "`Line-s_set` — flow pinned to the given schedule, wherever one is given" - dims: [snapshot, line] - where: Line_s_set + dims: [scenario, snapshot, line] + where: Line_s_set AND Line_active expression: Line_s == Line_s_set ``` ```math -s_{t,k} = \mathrm{s}^{\mathrm{set}}_{t,k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{s}^{\mathrm{set}}_{t,k} \text{ is defined} +s_{\xi,t,k} = \mathrm{s}^{\mathrm{set}}_{t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{s}^{\mathrm{set}}_{t,k} \text{ is defined} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Kirchhoff-Voltage-Law` @@ -2351,12 +2531,12 @@ Kirchhoff_Voltage_Law: `Kirchhoff-Voltage-Law` — around every independent cycle the impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport - dims: [snapshot, cycle] + dims: [scenario, snapshot, cycle] expression: sum(Line_s * Line_cycle_weight, over=line) == 0 ``` ```math -\sum_{k \in \mathcal{K}} s_{t,k} \cdot \mathrm{x}_{k,c} = 0 \qquad \forall\, t \in \mathcal{T},\ c \in \mathcal{C} +\sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{k,c} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ c \in \mathcal{C} ``` ### `Generator-p-ramp_limit_up` @@ -2371,16 +2551,16 @@ Generator_p_ramp_limit_up: start-up ramp in the snapshot it turns on. A unit that came into the horizon running brought an unknown output, so it carries no row at the first snapshot — nor does any unit a big M releases instead - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_ramp_limit_up AND NOT (Generator_committable AND Generator_p_nom_extendable) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) + AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance ``` ```math -p_{t,g} - \overleftarrow{p}_{t,g} \le \Delta^{+}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ru}_{g} \text{ is defined} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ru}_{g} \text{ is defined} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down` @@ -2395,16 +2575,16 @@ Generator_p_ramp_limit_down: shut-down ramp in the snapshot it turns off. A unit that came into the horizon running brought an unknown output, so it carries no row at the first snapshot — nor does any unit a big M releases instead - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_ramp_limit_down AND NOT (Generator_committable AND Generator_p_nom_extendable) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) + AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance ``` ```math -\overleftarrow{p}_{t,g} - p_{t,g} \le \Delta^{-}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{rd}_{g} \text{ is defined} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{rd}_{g} \text{ is defined} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Link-p-ramp_limit_up` @@ -2417,13 +2597,13 @@ Link_p_ramp_limit_up: `Link-p-ramp_limit_up` — a link raises flow no faster than its limit of the build. The translated term vacates the first snapshot, where a plain optimize builds no row either - dims: [snapshot, link] - where: Link_ramp_limit_up + dims: [scenario, snapshot, link] + where: Link_ramp_limit_up AND Link_active expression: Link_p - shift(Link_p, along=snapshot, offset=1) <= Link_ramp_limit_up * Link_p_nom_effective ``` ```math -f_{t,l} - f_{t - 1,l} \le \mathrm{ru}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ru}^{f}_{l} \text{ is defined} +f_{\xi,t,l} - f_{\xi,t - 1,l} \le \mathrm{ru}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down` @@ -2433,13 +2613,13 @@ f_{t,l} - f_{t - 1,l} \le \mathrm{ru}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mat ```yaml Link_p_ramp_limit_down: description: "`Link-p-ramp_limit_down` — a link lowers flow no faster than its limit of the build" - dims: [snapshot, link] - where: Link_ramp_limit_down + dims: [scenario, snapshot, link] + where: Link_ramp_limit_down AND Link_active expression: shift(Link_p, along=snapshot, offset=1) - Link_p <= Link_ramp_limit_down * Link_p_nom_effective ``` ```math -f_{t - 1,l} - f_{t,l} \le \mathrm{rd}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{rd}^{f}_{l} \text{ is defined} +f_{\xi,t - 1,l} - f_{\xi,t,l} \le \mathrm{rd}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} ``` ### `StorageUnit-ext-p_dispatch-lower` @@ -2449,13 +2629,13 @@ f_{t - 1,l} - f_{t,l} \le \mathrm{rd}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mat ```yaml StorageUnit_ext_p_dispatch_lower: description: "`StorageUnit-ext-p_dispatch-lower` — dispatch is non-negative" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch >= 0 ``` ```math -h^{+}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} +h^{+}_{\xi,t,s} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-p_dispatch-upper` @@ -2465,13 +2645,13 @@ h^{+}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \ ```yaml StorageUnit_ext_p_dispatch_upper: description: "`StorageUnit-ext-p_dispatch-upper` — an extendable unit dispatches at most the chosen build" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch <= StorageUnit_p_max_pu * StorageUnit_p_nom_ext ``` ```math -h^{+}_{t,s} \le \overline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} +h^{+}_{\xi,t,s} \le \overline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-p_store-lower` @@ -2481,13 +2661,13 @@ h^{+}_{t,s} \le \overline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, t \in \ ```yaml StorageUnit_ext_p_store_lower: description: "`StorageUnit-ext-p_store-lower` — storing is non-negative" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store >= 0 ``` ```math -h^{-}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} +h^{-}_{\xi,t,s} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-p_store-upper` @@ -2499,13 +2679,13 @@ StorageUnit_ext_p_store_upper: description: >- `StorageUnit-ext-p_store-upper` — an extendable unit stores at most the chosen build, the minimum-per-unit column carrying that cap negated - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store <= -StorageUnit_p_min_pu * StorageUnit_p_nom_ext ``` ```math -h^{-}_{t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} +h^{-}_{\xi,t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-state_of_charge-lower` @@ -2515,13 +2695,13 @@ h^{-}_{t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, t \in ```yaml StorageUnit_ext_state_of_charge_lower: description: "`StorageUnit-ext-state_of_charge-lower` — charge is non-negative" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge >= 0 ``` ```math -\mathit{soc}_{t,s} \ge 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} +\mathit{soc}_{\xi,t,s} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-state_of_charge-upper` @@ -2531,13 +2711,13 @@ StorageUnit_ext_state_of_charge_lower: ```yaml StorageUnit_ext_state_of_charge_upper: description: "`StorageUnit-ext-state_of_charge-upper` — an extendable unit holds at most its hours at the chosen build" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge <= StorageUnit_max_hours * StorageUnit_p_nom_ext ``` ```math -\mathit{soc}_{t,s} \le \mathrm{T}^{h}_{s} \cdot H_{s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} +\mathit{soc}_{\xi,t,s} \le \mathrm{T}^{h}_{s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-p_nom-lower` @@ -2598,7 +2778,8 @@ StorageUnit_energy_balance: `StorageUnit-energy_balance` — the charge carried in, plus what is stored after its efficiency, less what dispatch draws down before its own, plus inflow not spilled - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_active expression: >- StorageUnit_state_of_charge == StorageUnit_charge_carried_in @@ -2608,7 +2789,7 @@ StorageUnit_energy_balance: ``` ```math -\mathit{soc}_{t,s} = \overleftarrow{\mathit{soc}}_{t,s} + \eta^{-}_{s} \cdot h^{-}_{t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t} - \frac{h^{+}_{t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t}}{\eta^{+}_{s}} + \left( \mathrm{inflow}_{t,s} - \mathit{spill}_{t,s} \right) \cdot \mathrm{w}^{\mathrm{sto}}_{t} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} +\mathit{soc}_{\xi,t,s} = \overleftarrow{\mathit{soc}}_{\xi,t,s} + \eta^{-}_{s} \cdot h^{-}_{\xi,t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t} - \frac{h^{+}_{\xi,t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t}}{\eta^{+}_{s}} + \left( \mathrm{inflow}_{t,s} - \mathit{spill}_{\xi,t,s} \right) \cdot \mathrm{w}^{\mathrm{sto}}_{t} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{on}^{h}_{t,s} ``` ### `Store-fix-e-lower` @@ -2618,13 +2799,13 @@ StorageUnit_energy_balance: ```yaml Store_fix_e_lower: description: "`Store-fix-e-lower` — a fixed store holds at least its floor" - dims: [snapshot, store] - where: not Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: not Store_e_nom_extendable AND Store_active expression: Store_e >= Store_e_min_pu * Store_e_nom ``` ```math -e_{t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} +e_{\xi,t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-fix-e-upper` @@ -2634,13 +2815,13 @@ e_{t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qq ```yaml Store_fix_e_upper: description: "`Store-fix-e-upper` — a fixed store holds at most its nominal capacity" - dims: [snapshot, store] - where: not Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: not Store_e_nom_extendable AND Store_active expression: Store_e <= Store_e_max_pu * Store_e_nom ``` ```math -e_{t,v} \le \overline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} +e_{\xi,t,v} \le \overline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-ext-e-lower` @@ -2650,13 +2831,13 @@ e_{t,v} \le \overline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qqu ```yaml Store_ext_e_lower: description: "`Store-ext-e-lower` — an extendable store holds at least its floor of the chosen build" - dims: [snapshot, store] - where: Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: Store_e_nom_extendable AND Store_active expression: Store_e >= Store_e_min_pu * Store_e_nom_ext ``` ```math -e_{t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} +e_{\xi,t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-ext-e-upper` @@ -2666,13 +2847,13 @@ e_{t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, t \in \mat ```yaml Store_ext_e_upper: description: "`Store-ext-e-upper` — an extendable store holds at most the chosen build" - dims: [snapshot, store] - where: Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: Store_e_nom_extendable AND Store_active expression: Store_e <= Store_e_max_pu * Store_e_nom_ext ``` ```math -e_{t,v} \le \overline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} +e_{\xi,t,v} \le \overline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-ext-e_nom-lower` @@ -2730,7 +2911,8 @@ E_{v} = \mathrm{e}^{\mathrm{nom,set}}_{v} \qquad \forall\, v \in \mathcal{V} \,: ```yaml Store_energy_balance: description: "`Store-energy_balance` — the energy carried in, less what is delivered to the bus" - dims: [snapshot, store] + dims: [scenario, snapshot, store] + where: Store_active expression: >- Store_e == Store_energy_carried_in @@ -2738,7 +2920,7 @@ Store_energy_balance: ``` ```math -e_{t,v} = \overleftarrow{e}_{t,v} - q_{t,v} \cdot \mathrm{w}^{\mathrm{sto}}_{t} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} +e_{\xi,t,v} = \overleftarrow{e}_{\xi,t,v} - q_{\xi,t,v} \cdot \mathrm{w}^{\mathrm{sto}}_{t} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} ``` ### `Generator-p_set` @@ -2748,13 +2930,13 @@ e_{t,v} = \overleftarrow{e}_{t,v} - q_{t,v} \cdot \mathrm{w}^{\mathrm{sto}}_{t} ```yaml Generator_p_set: description: "`Generator-p_set` — output pinned to the given schedule, wherever one is given" - dims: [snapshot, generator] - where: Generator_p_set + dims: [scenario, snapshot, generator] + where: Generator_p_set AND Generator_active expression: Generator_p == Generator_p_set ``` ```math -p_{t,g} = \mathrm{p}^{\mathrm{set}}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{p}^{\mathrm{set}}_{t,g} \text{ is defined} +p_{\xi,t,g} = \mathrm{p}^{\mathrm{set}}_{t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{p}^{\mathrm{set}}_{t,g} \text{ is defined} \wedge \mathrm{on}_{t,g} ``` ### `Link-p_set` @@ -2764,13 +2946,13 @@ p_{t,g} = \mathrm{p}^{\mathrm{set}}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g ```yaml Link_p_set: description: "`Link-p_set` — flow pinned to the given schedule, wherever one is given" - dims: [snapshot, link] - where: Link_p_set + dims: [scenario, snapshot, link] + where: Link_p_set AND Link_active expression: Link_p == Link_p_set ``` ```math -f_{t,l} = \mathrm{f}^{\mathrm{set}}_{t,l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{f}^{\mathrm{set}}_{t,l} \text{ is defined} +f_{\xi,t,l} = \mathrm{f}^{\mathrm{set}}_{t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{f}^{\mathrm{set}}_{t,l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} ``` ### `StorageUnit-p_set` @@ -2780,13 +2962,13 @@ f_{t,l} = \mathrm{f}^{\mathrm{set}}_{t,l} \qquad \forall\, t \in \mathcal{T},\ l ```yaml StorageUnit_p_set: description: "`StorageUnit-p_set` — net dispatch pinned to the given schedule, wherever one is given" - dims: [snapshot, storage_unit] - where: StorageUnit_p_set + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_set AND StorageUnit_active expression: StorageUnit_p_dispatch - StorageUnit_p_store == StorageUnit_p_set ``` ```math -h^{+}_{t,s} - h^{-}_{t,s} = \mathrm{h}^{\mathrm{set}}_{t,s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{\mathrm{set}}_{t,s} \text{ is defined} +h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} = \mathrm{h}^{\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-state_of_charge_set` @@ -2796,13 +2978,13 @@ h^{+}_{t,s} - h^{-}_{t,s} = \mathrm{h}^{\mathrm{set}}_{t,s} \qquad \forall\, t \ ```yaml StorageUnit_state_of_charge_set: description: "`StorageUnit-state_of_charge_set` — charge pinned to the given schedule, wherever one is given" - dims: [snapshot, storage_unit] - where: StorageUnit_state_of_charge_set + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_state_of_charge_set AND StorageUnit_active expression: StorageUnit_state_of_charge == StorageUnit_state_of_charge_set ``` ```math -\mathit{soc}_{t,s} = \mathrm{soc}^{\mathrm{set}}_{t,s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{soc}^{\mathrm{set}}_{t,s} \text{ is defined} +\mathit{soc}_{\xi,t,s} = \mathrm{soc}^{\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{soc}^{\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} ``` ### `Store-e_set` @@ -2812,13 +2994,13 @@ StorageUnit_state_of_charge_set: ```yaml Store_e_set: description: "`Store-e_set` — energy pinned to the given schedule, wherever one is given" - dims: [snapshot, store] - where: Store_e_set + dims: [scenario, snapshot, store] + where: Store_e_set AND Store_active expression: Store_e == Store_e_set ``` ```math -e_{t,v} = \mathrm{e}^{\mathrm{set}}_{t,v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{e}^{\mathrm{set}}_{t,v} \text{ is defined} +e_{\xi,t,v} = \mathrm{e}^{\mathrm{set}}_{t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{e}^{\mathrm{set}}_{t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} ``` ### `primary_energy` @@ -2828,13 +3010,13 @@ e_{t,v} = \mathrm{e}^{\mathrm{set}}_{t,v} \qquad \forall\, t \in \mathcal{T},\ v ```yaml GlobalConstraint_primary_energy_ub: description: "`primary_energy` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '<=' expression: primary_energy <= GlobalConstraint_constant ``` ```math -\mathit{primary\_energy}_{b} \le \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} +\mathit{primary\_energy}_{\xi,b} \le \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} ``` ### `primary_energy` @@ -2844,13 +3026,13 @@ GlobalConstraint_primary_energy_ub: ```yaml GlobalConstraint_primary_energy_lb: description: "`primary_energy` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '>=' expression: primary_energy >= GlobalConstraint_constant ``` ```math -\mathit{primary\_energy}_{b} \ge \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} +\mathit{primary\_energy}_{\xi,b} \ge \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} ``` ### `primary_energy` @@ -2860,13 +3042,13 @@ GlobalConstraint_primary_energy_lb: ```yaml GlobalConstraint_primary_energy_eq: description: "`primary_energy` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '==' expression: primary_energy == GlobalConstraint_constant ``` ```math -\mathit{primary\_energy}_{b} = \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} +\mathit{primary\_energy}_{\xi,b} = \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} ``` ### `operational_limit` @@ -2876,13 +3058,13 @@ GlobalConstraint_primary_energy_eq: ```yaml GlobalConstraint_operational_limit_ub: description: "`operational_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'operational_limit' AND GlobalConstraint_sense == '<=' expression: operational_limit <= GlobalConstraint_constant ``` ```math -\mathit{operational\_limit}_{b} \le \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} +\mathit{operational\_limit}_{\xi,b} \le \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} ``` ### `operational_limit` @@ -2892,13 +3074,13 @@ GlobalConstraint_operational_limit_ub: ```yaml GlobalConstraint_operational_limit_lb: description: "`operational_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'operational_limit' AND GlobalConstraint_sense == '>=' expression: operational_limit >= GlobalConstraint_constant ``` ```math -\mathit{operational\_limit}_{b} \ge \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} +\mathit{operational\_limit}_{\xi,b} \ge \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} ``` ### `operational_limit` @@ -2908,13 +3090,13 @@ GlobalConstraint_operational_limit_lb: ```yaml GlobalConstraint_operational_limit_eq: description: "`operational_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'operational_limit' AND GlobalConstraint_sense == '==' expression: operational_limit == GlobalConstraint_constant ``` ```math -\mathit{operational\_limit}_{b} = \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} +\mathit{operational\_limit}_{\xi,b} = \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -3074,7 +3256,7 @@ Bus_nodal_balance: load there. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. - dims: [snapshot, bus] + dims: [scenario, snapshot, bus] expression: >- sum(Generator_p, by=Generator_bus, over=generator, into=bus) + sum(StorageUnit_p_dispatch - StorageUnit_p_store, by=StorageUnit_bus, over=storage_unit, into=bus) @@ -3087,7 +3269,60 @@ Bus_nodal_balance: ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{t,s} - h^{-}_{t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{t,o} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{t,k} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} +``` + +### `Carrier-growth_limit` + +`Carrier_growth_limit` + +```yaml +Carrier_growth_limit: + description: >- + `Carrier-growth_limit` — what a carrier adds in a period, counting each build in the first + period it stands in, is at most its allowance plus a share of what it added the period before; + the first period has no predecessor, so `edge=0` leaves it the bare allowance + dims: [carrier, period] + where: Carrier_max_growth + expression: >- + sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) + - shift(sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier), along=period, offset=1, edge=0) + * Carrier_max_relative_growth + <= Carrier_max_growth +``` + +```math +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} - \left( \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y \boxminus_{0} 1,g} \right) \cdot \mathrm{r}_{i} \le \overline{\Delta}_{i} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \overline{\Delta}_{i} \text{ is defined} +``` + +### `CVaR-excess-{s}` + +`CVaR_excess` + +```yaml +CVaR_excess: + description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" + dims: [scenario] + expression: CVaR_a - scenario_opex + CVaR_theta >= 0 +``` + +```math +a_{\xi} - \mathit{scenario\_opex}_{\xi} + \theta \ge 0 \qquad \forall\, \xi \in \Xi +``` + +### `CVaR-def` + +`CVaR_def` + +```yaml +CVaR_def: + description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" + dims: [] + expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR +``` + +```math +\theta + \mathrm{v} \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot a_{\xi} \right) \le CVaR ``` ### `Generator_previous_status` @@ -3097,14 +3332,14 @@ Generator_previous_status: description: >- the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: opening: { when: "position(snapshot) == 0", expression: Generator_status_initial } otherwise: shift(Generator_status, along=snapshot, offset=1) ``` ```math -\overleftarrow{u}_{t,g} = \begin{cases} \mathrm{u}^{0}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ u_{t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\overleftarrow{u}_{\xi,t,g} = \begin{cases} \mathrm{u}^{0}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ u_{\xi,t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_previous_p` @@ -3115,14 +3350,14 @@ Generator_previous_p: the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: opening: { when: "position(snapshot) == 0", expression: 0 } otherwise: shift(Generator_p, along=snapshot, offset=1) ``` ```math -\overleftarrow{p}_{t,g} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ p_{t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\overleftarrow{p}_{\xi,t,g} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ p_{\xi,t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_p_nom_effective` @@ -3148,7 +3383,7 @@ Generator_ramp_up_allowance: how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: committed: when: Generator_committable @@ -3160,7 +3395,7 @@ Generator_ramp_up_allowance: ``` ```math -\Delta^{+}_{t,g} = \begin{cases} \mathrm{ru}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{t,g} + \mathrm{ru}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{t,g} - \overleftarrow{u}_{t,g} \right) & \text{if } \mathrm{com}_{g} \\ \mathrm{ru}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{+}_{\xi,t,g} = \begin{cases} \mathrm{ru}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{\xi,t,g} + \mathrm{ru}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \mathrm{ru}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_ramp_down_allowance` @@ -3171,7 +3406,7 @@ Generator_ramp_down_allowance: how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: committed: when: Generator_committable @@ -3183,7 +3418,7 @@ Generator_ramp_down_allowance: ``` ```math -\Delta^{-}_{t,g} = \begin{cases} \mathrm{rd}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} + \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{t,g} - u_{t,g} \right) & \text{if } \mathrm{com}_{g} \\ \mathrm{rd}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{-}_{\xi,t,g} = \begin{cases} \mathrm{rd}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} + \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \mathrm{rd}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Link_p_nom_effective` @@ -3210,7 +3445,7 @@ StorageUnit_charge_carried_in: standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] cases: cyclic: when: StorageUnit_cyclic_state_of_charge @@ -3222,7 +3457,7 @@ StorageUnit_charge_carried_in: ``` ```math -\overleftarrow{\mathit{soc}}_{t,s} = \begin{cases} \rho_{t,s} \cdot \mathit{soc}_{t \ominus 1,s} & \text{if } \mathrm{cyc}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \neg \mathrm{cyc}_{s} \wedge \mathrm{pos}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} +\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus 1,s} & \text{if } \mathrm{cyc}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \neg \mathrm{cyc}_{s} \wedge \mathrm{pos}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} ``` ### `Store_energy_carried_in` @@ -3234,7 +3469,7 @@ Store_energy_carried_in: standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise - dims: [snapshot, store] + dims: [scenario, snapshot, store] cases: cyclic: when: Store_e_cyclic @@ -3246,7 +3481,7 @@ Store_energy_carried_in: ``` ```math -\overleftarrow{e}_{t,v} = \begin{cases} \rho^{e}_{t,v} \cdot e_{t \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \neg \mathrm{cyc}^{e}_{v} \wedge \mathrm{pos}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} +\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{t,v} \cdot e_{\xi,t \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \neg \mathrm{cyc}^{e}_{v} \wedge \mathrm{pos}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} ``` ### `Link_output_arrival` @@ -3260,7 +3495,7 @@ Link_output_arrival: is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not - dims: [snapshot, link_output] + dims: [scenario, snapshot, link_output] cases: wrapping: when: Link_output_cyclic_delay @@ -3269,7 +3504,7 @@ Link_output_arrival: ``` ```math -\overrightarrow{f}_{t,o} = \begin{cases} f_{t \ominus \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{t \boxminus_{0} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ o \in \mathcal{O} +\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} ``` ### `primary_energy` @@ -3287,7 +3522,7 @@ primary_energy: ``` ```math -\mathit{primary\_energy}_{b} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{a}_{b,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{h}_{b,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{e}_{b,v} \right) \qquad \forall\, b \in \mathcal{B} +\mathit{primary\_energy}_{\xi,b} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{a}_{b,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{h}_{b,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{e}_{b,v} \right) \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} ``` ### `operational_limit` @@ -3305,7 +3540,7 @@ operational_limit: ``` ```math -\mathit{operational\_limit}_{b} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{b}_{b,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{h}_{b,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{e}_{b,v} \right) \qquad \forall\, b \in \mathcal{B} +\mathit{operational\_limit}_{\xi,b} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{b}_{b,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{h}_{b,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{e}_{b,v} \right) \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} ``` ### `transmission_volume_expansion` @@ -3353,54 +3588,76 @@ tech_capacity_expansion: \mathit{tech\_capacity\_expansion}_{b} = \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{m}_{b,g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{m}^{f}_{b,l} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{m}^{l}_{b,k} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{m}^{h}_{b,s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{m}^{e}_{b,v} \qquad \forall\, b \in \mathcal{B} ``` +### `scenario_opex` + +```yaml +scenario_opex: + description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight + expression: >- + sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(Store_p * Store_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + + sum(sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_shut_down * Generator_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) +``` + +```math +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +``` + #### Variable domains **`Generator_p`** ```math -p_{t,g} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +p_{\xi,t,g} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{on}_{t,g} ``` **`Link_p`** ```math -f_{t,l} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} +f_{\xi,t,l} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{on}^{f}_{t,l} ``` **`StorageUnit_p_dispatch`** ```math -h^{+}_{t,s} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} +h^{+}_{\xi,t,s} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{on}^{h}_{t,s} ``` **`StorageUnit_p_store`** ```math -h^{-}_{t,s} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} +h^{-}_{\xi,t,s} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{on}^{h}_{t,s} ``` **`StorageUnit_state_of_charge`** ```math -\mathit{soc}_{t,s} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} +\mathit{soc}_{\xi,t,s} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{on}^{h}_{t,s} ``` **`StorageUnit_spill`** ```math -0 \le \mathit{spill}_{t,s} \le \mathrm{inflow}_{t,s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{inflow}_{t,s} > 0 +0 \le \mathit{spill}_{\xi,t,s} \le \mathrm{inflow}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{inflow}_{t,s} > 0 \wedge \mathrm{on}^{h}_{t,s} ``` **`Store_e`** ```math -e_{t,v} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} +e_{\xi,t,v} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} ``` **`Store_p`** ```math -q_{t,v} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} +q_{\xi,t,v} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} ``` **`Generator_n_mod`** @@ -3412,25 +3669,25 @@ N_{g} \ge 0, N_{g} \in \mathbb{Z} \qquad \forall\, g \in \mathcal{G} \,:\, \math **`Generator_status`** ```math -u_{t,g} \ge 0, u_{t,g} \in \mathbb{Z} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} +u_{\xi,t,g} \ge 0, u_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` **`Generator_start_up`** ```math -\mathit{up}_{t,g} \ge 0, \mathit{up}_{t,g} \in \mathbb{Z} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} +\mathit{up}_{\xi,t,g} \ge 0, \mathit{up}_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` **`Generator_shut_down`** ```math -\mathit{dn}_{t,g} \ge 0, \mathit{dn}_{t,g} \in \mathbb{Z} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} +\mathit{dn}_{\xi,t,g} \ge 0, \mathit{dn}_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` **`Line_s`** ```math -s_{t,k} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} +s_{\xi,t,k} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{on}^{s}_{t,k} ``` **`Line_s_nom_ext`** @@ -3462,6 +3719,24 @@ H_{s} \in \mathbb{R} \qquad \forall\, s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{ ```math E_{v} \in \mathbb{R} \qquad \forall\, v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} ``` + +**`CVaR_a`** + +```math +a_{\xi} \ge 0 \qquad \forall\, \xi \in \Xi +``` + +**`CVaR_theta`** + +```math +\theta \in \mathbb{R} +``` + +**`CVaR`** + +```math +CVaR \in \mathbb{R} +``` Regenerate with `pixi run python -m tools.gallery`. diff --git a/docs/examples/pypsa_multi_period.md b/docs/examples/pypsa_multi_period.md deleted file mode 100644 index b2fa3c07..00000000 --- a/docs/examples/pypsa_multi_period.md +++ /dev/null @@ -1,372 +0,0 @@ - - -# PyPSA, the multi-period class - -Rung 15 of [PyPSA in one file](pypsa.md): `n.optimize(multi_investment_periods=True)`, stated on rungs 1 and 3 in a -file of its own. Its network is a whole one: eight snapshots over two investment periods, build years and lifetimes on the script. - -## Rung 15 — investment periods, with a growth limit - -| PyPSA | status | note | -| --- | --- | --- | -| [`Generator-p`](#variable-domains) | done | where the generator stands in the snapshot's period — `active`, data prep | -| [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | -| [`Carrier-growth_limit`](#carrier-growth_limit) | done | counted in the first period a build stands in; `edge=0` at the first period | -| [objective](#objective) | done | period weight on operation; capacity once per period it stands in | -| `StorageUnit-energy_balance` per period, ramps at period starts | out | `shift(…, by=snapshot_period, within=period)` has them; a later rung | - - -> ✔ `pypsa 1.3.0` solves this rung's network at objective `12747.19109626398`, 80 rows. - -
-The network, as PyPSA code - -`rung_15_multi_period.py` - -```python -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -"""Rung 15: two investment periods — build years, lifetimes, period weights and a carrier's growth limit, stated by `pypsa_multi_period.yaml`.""" - -from __future__ import annotations - -from datetime import datetime - -import pandas as pd - -MODEL = 'pypsa_multi_period.yaml' -OPTIMIZE = {'multi_investment_periods': True} - - -def build(): - """A whole network, not the spine: eight snapshots over two periods, a unit that retires, two wind builds capped by growth.""" - import pypsa - - n = pypsa.Network() - n.snapshots = pd.MultiIndex.from_tuples( - [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] - ) - n.investment_periods = [2020, 2030] - n.investment_period_weightings['objective'] = [1.0, 0.5] - n.investment_period_weightings['years'] = [10.0, 10.0] - n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 2.0, 1.5, 2.5, 2.0] - n.add('Bus', ['north', 'south']) - n.add('Carrier', 'wind', max_growth=50, max_relative_growth=0.5) - n.add('Carrier', 'gas') - n.add('Generator', 'old_gas', bus='north', carrier='gas', p_nom=40, marginal_cost=30, build_year=2010, lifetime=15) - n.add( - 'Generator', - 'wind20', - bus='north', - carrier='wind', - p_nom_extendable=True, - p_nom_max=200, - marginal_cost=1, - capital_cost=100, - build_year=2020, - lifetime=30, - p_max_pu=[0.8, 0.6, 0.7, 0.5, 0.8, 0.6, 0.7, 0.5], - ) - n.add( - 'Generator', - 'wind30', - bus='south', - carrier='wind', - p_nom_extendable=True, - p_nom_max=200, - marginal_cost=1, - capital_cost=80, - build_year=2030, - lifetime=30, - p_max_pu=[0.9, 0.7, 0.6, 0.8, 0.9, 0.7, 0.6, 0.8], - ) - n.add( - 'Generator', - 'gas30', - bus='south', - carrier='gas', - p_nom_extendable=True, - p_nom_max=200, - marginal_cost=40, - capital_cost=50, - build_year=2030, - lifetime=30, - ) - n.add('Link', 'wire15', bus0='north', bus1='south', p_nom=60, p_min_pu=-1, efficiency=0.95) - n.add('Load', 'town15', bus='north', p_set=[20, 30, 25, 20, 35, 45, 40, 30]) - n.add('Load', 'port15', bus='south', p_set=[10, 20, 15, 10, 30, 40, 35, 25]) - return n -``` - -
- - -## The file - - -The multi-period class of a plain `n.optimize()`: `multi_investment_periods`, stated on rungs 1 and 3 in a file of its own. A snapshot belongs to an investment period, an asset stands in the periods its build year and lifetime span, and capacity is paid once per period it stands in, each period weighted; a carrier may grow only so much per period. Which snapshots an asset is active in is data prep, because a `where` reaches only the frame's own dimensions. A dimension a run may not have cannot ride on `examples/pypsa.yaml`, so this class lives here. - -#### Sets - -| Symbol | Meaning | -|---|---| -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods, positions across every investment period | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{C},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | -| $`\mathcal{C}`$ | index $`c`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{C}`$ — energy carriers, what a growth limit is set per | - -#### Parameters - -| Symbol | Meaning | -|---|---| -| $`\mathrm{w}`$ | `snapshot_weightings_objective` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost | -| $`\mathrm{w}^{y}`$ | `period_weight_objective` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.objective` — what a period's cost weighs | -| $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep | -| $`\mathrm{W}`$ | `Generator_capital_weight` over $`\mathcal{G}`$ — the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep | -| $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{C}`$ — most capacity of a carrier that may be added in a period; no value means no limit | -| $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{C}`$ — share of the previous period's additions that may be added on top | -| $`\mathrm{p}^{\mathrm{nom}}`$ | `Generator_p_nom` over $`\mathcal{G}`$ — nominal power | -| $`\mathrm{ext}`$ | `Generator_p_nom_extendable` over $`\mathcal{G}`$ — whether the nominal power is a decision | -| $`\underline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_min` over $`\mathcal{G}`$ — least nominal power an extendable generator may be built at | -| $`\overline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_max` over $`\mathcal{G}`$ — most nominal power an extendable generator may be built at | -| $`\mathrm{c}^{\mathrm{cap}}`$ | `Generator_capital_cost` over $`\mathcal{G}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | -| $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | -| $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | -| $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\mathcal{L}`$ — nominal power | -| $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | -| $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | -| $`\eta`$ | `Link_efficiency` over $`\mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | -| $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | -| $`\mathrm{load}`$ | `Load_p_set` over $`\mathcal{T} \times \mathcal{D}`$ — demand | - -#### Variables - -| Symbol | Meaning | -|---|---| -| $`p`$ | `Generator_p` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-p` — output of a generator in a snapshot | -| $`f`$ | `Link_p` over $`\mathcal{T} \times \mathcal{L}`$ — `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to | -| $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | - -$`t \boxminus_{v} k`$ denotes translation with $`v`$ standing where index $`t-k`$ leaves the dimension (`shift(edge=v)`), so the row at that boundary is built and carries $`v`$ rather than being dropped. - -### Objective - -```yaml -objective: - sense: minimize - description: operating cost by weighted snapshot and weighted period, and capacity once per period it stands in - expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot)) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot)) - + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) -``` - -```math -\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T},\ l \in \mathcal{L}} f_{t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} -``` - -### `Generator-fix-p-lower` - -`Generator_fix_p_lower` - -```yaml -Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom -``` - -```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} -``` - -### `Generator-fix-p-upper` - -`Generator_fix_p_upper` - -```yaml -Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom -``` - -```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} -``` - -### `Generator-ext-p-lower` - -`Generator_ext_p_lower` - -```yaml -Generator_ext_p_lower: - description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext -``` - -```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} -``` - -### `Generator-ext-p-upper` - -`Generator_ext_p_upper` - -```yaml -Generator_ext_p_upper: - description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext -``` - -```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} -``` - -### `Generator-ext-p_nom-lower` - -`Generator_ext_p_nom_lower` - -```yaml -Generator_ext_p_nom_lower: - description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [generator] - where: Generator_p_nom_extendable - expression: Generator_p_nom_ext >= Generator_p_nom_min -``` - -```math -P_{g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} -``` - -### `Generator-ext-p_nom-upper` - -`Generator_ext_p_nom_upper` - -```yaml -Generator_ext_p_nom_upper: - description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [generator] - where: Generator_p_nom_extendable AND Generator_p_nom_max - expression: Generator_p_nom_ext <= Generator_p_nom_max -``` - -```math -P_{g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \text{ is defined} -``` - -### `Link-fix-p-lower` - -`Link_fix_p_lower` - -```yaml -Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom -``` - -```math -f_{t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Link-fix-p-upper` - -`Link_fix_p_upper` - -```yaml -Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom -``` - -```math -f_{t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Bus-nodal_balance` - -`Bus_nodal_balance` - -```yaml -Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, less what the links - take away, plus what arrives over them after losses, meets the load - there - dims: [snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) -``` - -```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{t,g} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} f_{t,\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} -``` - -### `Carrier-growth_limit` - -`Carrier_growth_limit` - -```yaml -Carrier_growth_limit: - description: >- - `Carrier-growth_limit` — what a carrier adds in a period, counting each - build in the first period it stands in, is at most its allowance plus a - share of what it added the period before; the first period has no - predecessor, so `edge=0` leaves it the bare allowance - dims: [carrier, period] - where: Carrier_max_growth - expression: >- - sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) - - shift(sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier), along=period, offset=1, edge=0) - * Carrier_max_relative_growth - <= Carrier_max_growth -``` - -```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = c} P_{g} \cdot \mathrm{new}_{y,g} - \left( \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = c} P_{g} \cdot \mathrm{new}_{y \boxminus_{0} 1,g} \right) \cdot \mathrm{r}_{c} \le \overline{\Delta}_{c} \qquad \forall\, c \in \mathcal{C},\ y \in \mathcal{Y} \,:\, \overline{\Delta}_{c} \text{ is defined} -``` - -#### Variable domains - -**`Generator_p`** - -```math -p_{t,g} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{on}_{t,g} -``` - -**`Link_p`** - -```math -f_{t,l} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -**`Generator_p_nom_ext`** - -```math -P_{g} \in \mathbb{R} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} -``` - - -Regenerate with `pixi run python -m tools.gallery`. diff --git a/docs/examples/pypsa_stochastic.md b/docs/examples/pypsa_stochastic.md deleted file mode 100644 index c631c8d4..00000000 --- a/docs/examples/pypsa_stochastic.md +++ /dev/null @@ -1,365 +0,0 @@ - - -# PyPSA, the two-stage class - -Rung 14 of [PyPSA in one file](pypsa.md): `n.set_scenarios(...)` with `n.set_risk_preference(alpha, omega)`, stated on rungs 1 and 3 in a -file of its own. Its network is the spine plus the script's own additions. - -## Rung 14 — two-stage stochastic, with CVaR - -| PyPSA | status | note | -| --- | --- | --- | -| [`Generator-p`, `Link-p`](#variable-domains) | done | over `scenario`; `Generator-p_nom` is not — chosen once | -| [`Generator-fix-p-*`, `-ext-p-*`, `Link-fix-p-*`, `Bus-nodal_balance`](#generator-fix-p-lower) | done | rungs 1 and 3, over `scenario` | -| [`CVaR-a`, `CVaR-theta`, `CVaR`](#variable-domains) | done | | -| [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension | -| [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep | -| [objective](#objective) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | - - -> ✔ `pypsa 1.3.0` solves this rung's network at objective `9267.386666666665`, 87 rows. - -
-The network, as PyPSA code - -`rung_14_stochastic.py` - -```python -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -"""Rung 14: two futures and a risk preference — capacity chosen once, dispatch per scenario, stated by `pypsa_stochastic.yaml`.""" - -from __future__ import annotations - -import spine - -MODEL = 'pypsa_stochastic.yaml' - - -def build(): - """The spine plus an extendable wind unit whose availability and the south's load differ between a calm and a stormy future.""" - n = spine.build() - n.add('Generator', 'wind14', bus='south', p_nom_extendable=True, p_nom_max=100, marginal_cost=1, capital_cost=20) - n.add('Load', 'port14', bus='south') - n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) - n.c.loads.dynamic.p_set[('calm', 'port14')] = [10, 20, 15, 10] - n.c.loads.dynamic.p_set[('stormy', 'port14')] = [40, 60, 50, 30] - n.c.generators.dynamic.p_max_pu[('calm', 'wind14')] = [0.9, 0.7, 0.8, 0.6] - n.c.generators.dynamic.p_max_pu[('stormy', 'wind14')] = [0.3, 0.2, 0.4, 0.1] - n.set_risk_preference(alpha=0.5, omega=0.3) - return n -``` - -
- - -## The file - - -The two-stage class of a plain `n.optimize()`: a network with scenarios, stated on rung 1's transport and rung 3's expansion in a file of its own. Everything over a snapshot spans a scenario as well; capacity does not — it is chosen once, before the future is known — and the cost is the expectation over the scenarios' weights. With a risk preference PyPSA adds the CVaR rows: an excess per scenario and the tail's average, blended into the objective. A dimension a run may not have cannot ride on `examples/pypsa.yaml`, so this class lives here. - -#### Sets - -| Symbol | Meaning | -|---|---| -| $`\mathcal{S}`$ | index $`s`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | - -#### Parameters - -| Symbol | Meaning | -|---|---| -| $`\pi`$ | `scenario_weight` over $`\mathcal{S}`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | -| $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of the operating cost priced at the tail rather than in expectation | -| $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor | -| $`\mathrm{w}`$ | `snapshot_weightings_objective` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost | -| $`\mathrm{p}^{\mathrm{nom}}`$ | `Generator_p_nom` over $`\mathcal{G}`$ — nominal power | -| $`\mathrm{ext}`$ | `Generator_p_nom_extendable` over $`\mathcal{G}`$ — whether the nominal power is a decision | -| $`\underline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_min` over $`\mathcal{G}`$ — least nominal power an extendable generator may be built at | -| $`\overline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_max` over $`\mathcal{G}`$ — most nominal power an extendable generator may be built at | -| $`\mathrm{c}^{\mathrm{cap}}`$ | `Generator_capital_cost` over $`\mathcal{G}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | -| $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{S} \times \mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | -| $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | -| $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\mathcal{L}`$ — nominal power | -| $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | -| $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | -| $`\eta`$ | `Link_efficiency` over $`\mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | -| $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | -| $`\mathrm{load}`$ | `Load_p_set` over $`\mathcal{S} \times \mathcal{T} \times \mathcal{D}`$ — demand | - -#### Variables - -| Symbol | Meaning | -|---|---| -| $`p`$ | `Generator_p` over $`\mathcal{S} \times \mathcal{T} \times \mathcal{G}`$ — `Generator-p` — output of a generator in a snapshot | -| $`f`$ | `Link_p` over $`\mathcal{S} \times \mathcal{T} \times \mathcal{L}`$ — `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to | -| $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | -| $`a`$ | `CVaR_a` over $`\mathcal{S}`$ — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not | -| $`\theta`$ | `CVaR_theta` (scalar) — `CVaR-theta` — where the tail starts, the value at risk | -| $`CVaR`$ | `CVaR` (scalar) — `CVaR` — the tail's average cost, what the objective prices at `omega` | - -#### Definitions - -| Symbol | Meaning | -|---|---| -| $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\mathcal{S}`$ — what a future costs to run — the operating terms, before their weight | - -### Objective - -```yaml -objective: - sense: minimize - description: capacity once, operation in expectation, and a share of it at the tail - expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) - + CVaR_omega * CVaR -``` - -```math -\min \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} + \left( 1 - \omega \right) \cdot \left( \sum_{s \in \mathcal{S}} \pi_{s} \cdot \mathit{scenario\_opex}_{s} \right) + \omega \cdot CVaR -``` - -### `Generator-fix-p-lower` - -`Generator_fix_p_lower` - -```yaml -Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [scenario, snapshot, generator] - where: not Generator_p_nom_extendable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom -``` - -```math -p_{s,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} -``` - -### `Generator-fix-p-upper` - -`Generator_fix_p_upper` - -```yaml -Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [scenario, snapshot, generator] - where: not Generator_p_nom_extendable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom -``` - -```math -p_{s,t,g} \le \overline{\mathrm{p}}_{s,t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} -``` - -### `Generator-ext-p-lower` - -`Generator_ext_p_lower` - -```yaml -Generator_ext_p_lower: - description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" - dims: [scenario, snapshot, generator] - where: Generator_p_nom_extendable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext -``` - -```math -p_{s,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} -``` - -### `Generator-ext-p-upper` - -`Generator_ext_p_upper` - -```yaml -Generator_ext_p_upper: - description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" - dims: [scenario, snapshot, generator] - where: Generator_p_nom_extendable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext -``` - -```math -p_{s,t,g} \le \overline{\mathrm{p}}_{s,t,g} \cdot P_{g} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} -``` - -### `Generator-ext-p_nom-lower` - -`Generator_ext_p_nom_lower` - -```yaml -Generator_ext_p_nom_lower: - description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [generator] - where: Generator_p_nom_extendable - expression: Generator_p_nom_ext >= Generator_p_nom_min -``` - -```math -P_{g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} -``` - -### `Generator-ext-p_nom-upper` - -`Generator_ext_p_nom_upper` - -```yaml -Generator_ext_p_nom_upper: - description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [generator] - where: Generator_p_nom_extendable AND Generator_p_nom_max - expression: Generator_p_nom_ext <= Generator_p_nom_max -``` - -```math -P_{g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \text{ is defined} -``` - -### `Link-fix-p-lower` - -`Link_fix_p_lower` - -```yaml -Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [scenario, snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom -``` - -```math -f_{s,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Link-fix-p-upper` - -`Link_fix_p_upper` - -```yaml -Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [scenario, snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom -``` - -```math -f_{s,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Bus-nodal_balance` - -`Bus_nodal_balance` - -```yaml -Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, less what the links - take away, plus what arrives over them after losses, meets the load - there - dims: [scenario, snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) -``` - -```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{s,t,g} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{s,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} f_{s,t,\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{s,t,d} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ n \in \mathcal{N} -``` - -### `CVaR-excess-{s}` - -`CVaR_excess` - -```yaml -CVaR_excess: - description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" - dims: [scenario] - expression: CVaR_a - scenario_opex + CVaR_theta >= 0 -``` - -```math -a_{s} - \mathit{scenario\_opex}_{s} + \theta \ge 0 \qquad \forall\, s \in \mathcal{S} -``` - -### `CVaR-def` - -`CVaR_def` - -```yaml -CVaR_def: - description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" - dims: [] - expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR -``` - -```math -\theta + \mathrm{v} \cdot \left( \sum_{s \in \mathcal{S}} \pi_{s} \cdot a_{s} \right) \le CVaR -``` - -### `scenario_opex` - -```yaml -scenario_opex: - description: what a future costs to run — the operating terms, before their weight - expression: >- - sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective, over=generator), over=snapshot) - + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective, over=link), over=snapshot) -``` - -```math -\mathit{scenario\_opex}_{s} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{s,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{s,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \qquad \forall\, s \in \mathcal{S} -``` - -#### Variable domains - -**`Generator_p`** - -```math -p_{s,t,g} \in \mathbb{R} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ g \in \mathcal{G} -``` - -**`Link_p`** - -```math -f_{s,t,l} \in \mathbb{R} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ l \in \mathcal{L} -``` - -**`Generator_p_nom_ext`** - -```math -P_{g} \in \mathbb{R} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} -``` - -**`CVaR_a`** - -```math -a_{s} \ge 0 \qquad \forall\, s \in \mathcal{S} -``` - -**`CVaR_theta`** - -```math -\theta \in \mathbb{R} -``` - -**`CVaR`** - -```math -CVaR \in \mathbb{R} -``` - - -Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 663e07de..f4640fb7 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -3,16 +3,19 @@ # SPDX-License-Identifier: MIT description: >- - The model a plain `n.optimize()` builds, stated in one file. Every - declaration is named `Component_attribute` after the PyPSA statement it - stands for, and each constraint's description opens with the linopy name - PyPSA gives that row, so the two can be read side by side. PyPSA's regimes - — extendable, committable — are data columns and become `where:` masks. - Bounds are the explicit rows PyPSA writes, so their duals are row duals. - Parameters no PyPSA table carries verbatim are computed in data prep and say - so in their description. + A plain `n.optimize()`, and its multi-period and stochastic classes, in one + file. Every second-stage quantity spans a `scenario` (a future dispatch is + chosen in) and every asset stands in the investment `period`s its build year + and lifetime span. Capacity is chosen once, before the future is known, and + paid once per active period; operation is the expectation over the scenarios' + weights, with a share priced at the tail through the CVaR rows. A plain run + feeds one scenario, one period, all-active masks and unit weights, and the + model collapses to the standard one. Which snapshots an asset is active in, + and a scenario's weight, are data prep. dimensions: + scenario: + description: the futures dispatch is chosen in, each with a weight snapshot: description: dispatch periods dtype: datetime @@ -39,6 +42,11 @@ dimensions: description: independent cycles of the passive network graph — the cycle basis, data prep global_constraint: description: PyPSA's `GlobalConstraint` rows, one label per declared limit + period: + description: investment periods — PyPSA's `investment_periods` + dtype: int + carrier: + description: energy carriers, what a growth limit is set per parameters: snapshot_weightings_objective: @@ -167,7 +175,63 @@ parameters: dims: [snapshot, link] Load_p_set: description: demand - dims: [snapshot, load] + dims: [scenario, snapshot, load] + scenario_weight: + description: PyPSA's `scenario_weightings.weight` — the probability of a future + dims: [scenario] + CVaR_omega: + description: PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model + dims: [] + CVaR_inv_tail: + description: PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor + dims: [] + period_weight_objective: + description: PyPSA's `investment_period_weightings.objective` — what a period's cost weighs + dims: [period] + Generator_active: + description: whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep + dims: [snapshot, generator] + dtype: bool + Link_active: + description: whether a link stands in a snapshot's period — PyPSA's `active`, data prep + dims: [snapshot, link] + dtype: bool + StorageUnit_active: + description: whether a storage unit stands in a snapshot's period — PyPSA's `active`, data prep + dims: [snapshot, storage_unit] + dtype: bool + Store_active: + description: whether a store stands in a snapshot's period — PyPSA's `active`, data prep + dims: [snapshot, store] + dtype: bool + Line_active: + description: whether a line stands in a snapshot's period — PyPSA's `active`, data prep + dims: [snapshot, line] + dtype: bool + Generator_capital_weight: + description: the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep + dims: [generator] + Link_capital_weight: + description: the sum of period weights a link stands in — PyPSA's `active * period_weighting`, summed, data prep + dims: [link] + StorageUnit_capital_weight: + description: the sum of period weights a storage unit stands in — PyPSA's `active * period_weighting`, summed, data prep + dims: [storage_unit] + Store_capital_weight: + description: the sum of period weights a store stands in — PyPSA's `active * period_weighting`, summed, data prep + dims: [store] + Line_capital_weight: + description: the sum of period weights a line stands in — PyPSA's `active * period_weighting`, summed, data prep + dims: [line] + Generator_first_active: + description: one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + dims: [period, generator] + Carrier_max_growth: + description: most capacity of a carrier that may be added in a period; no value means no limit + dims: [carrier] + Carrier_max_relative_growth: + description: share of the previous period's additions that may be added on top + dims: [carrier] Generator_p_set: description: a given output schedule; a generator without one has no row here dims: [snapshot, generator] @@ -420,6 +484,14 @@ parameters: dims: [global_constraint, store] relations: + snapshot_period: + description: the investment period a snapshot falls in + key: snapshot + values: period + Generator_carrier: + description: the carrier a generator converts from + key: generator + values: carrier Generator_bus: description: the bus a generator sits on key: generator @@ -463,39 +535,46 @@ relations: variables: Generator_p: description: "`Generator-p` — output of a generator in a snapshot" - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] + where: Generator_active Link_p: description: >- `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to - dims: [snapshot, link] + dims: [scenario, snapshot, link] + where: Link_active StorageUnit_p_dispatch: description: "`StorageUnit-p_dispatch` — power delivered to the bus" - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_active StorageUnit_p_store: description: "`StorageUnit-p_store` — power drawn from the bus into charge" - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_active StorageUnit_state_of_charge: description: "`StorageUnit-state_of_charge` — energy held at the end of a snapshot" - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_active StorageUnit_spill: description: >- `StorageUnit-spill` — inflow passed on unused. Zero where there is no inflow, so the balance keeps its row there; the bounds are PyPSA's, on the variable rather than as rows - dims: [snapshot, storage_unit] - where: "StorageUnit_inflow > 0" + dims: [scenario, snapshot, storage_unit] + where: "StorageUnit_inflow > 0 AND StorageUnit_active" absence: zero bounds: lower: 0 upper: StorageUnit_inflow Store_e: description: "`Store-e` — energy held at the end of a snapshot" - dims: [snapshot, store] + dims: [scenario, snapshot, store] + where: Store_active Store_p: description: "`Store-p` — power delivered to the bus; charging is negative" - dims: [snapshot, store] + dims: [scenario, snapshot, store] + where: Store_active Generator_n_mod: description: "`Generator-n_mod` — how many modules of an extendable modular build" dims: [generator] @@ -508,22 +587,22 @@ variables: `Generator-status` — how much of a committable unit is on: an integer the rows below cap at one, or at the module count where the build is modular - dims: [snapshot, generator] - where: Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_active domain: integer bounds: lower: 0 Generator_start_up: description: "`Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is" - dims: [snapshot, generator] - where: Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_active domain: integer bounds: lower: 0 Generator_shut_down: description: "`Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is" - dims: [snapshot, generator] - where: Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_active domain: integer bounds: lower: 0 @@ -531,7 +610,8 @@ variables: description: >- `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless - dims: [snapshot, line] + dims: [scenario, snapshot, line] + where: Line_active Line_s_nom_ext: description: >- `Line-s_nom` — nominal apparent power where it is a decision; the @@ -562,13 +642,24 @@ variables: of the same PyPSA name carries the fixed regime dims: [store] where: Store_e_nom_extendable + CVaR_a: + description: "`CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not" + dims: [scenario] + bounds: + lower: 0 + CVaR_theta: + description: "`CVaR-theta` — where the tail starts, the value at risk" + dims: [] + CVaR: + description: "`CVaR` — the tail's average cost, what the objective prices at `omega`" + dims: [] expressions: Generator_previous_status: description: >- the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: opening: { when: "position(snapshot) == 0", expression: Generator_status_initial } otherwise: shift(Generator_status, along=snapshot, offset=1) @@ -577,7 +668,7 @@ expressions: the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: opening: { when: "position(snapshot) == 0", expression: 0 } otherwise: shift(Generator_p, along=snapshot, offset=1) @@ -592,7 +683,7 @@ expressions: how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: committed: when: Generator_committable @@ -606,7 +697,7 @@ expressions: how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] cases: committed: when: Generator_committable @@ -627,7 +718,7 @@ expressions: standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] cases: cyclic: when: StorageUnit_cyclic_state_of_charge @@ -642,7 +733,7 @@ expressions: standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise - dims: [snapshot, store] + dims: [scenario, snapshot, store] cases: cyclic: when: Store_e_cyclic @@ -659,7 +750,7 @@ expressions: is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not - dims: [snapshot, link_output] + dims: [scenario, snapshot, link_output] cases: wrapping: when: Link_output_cyclic_delay @@ -701,37 +792,50 @@ expressions: + sum(Line_s_nom_ext * Line_tech_capacity_weight, over=line) + sum(StorageUnit_p_nom_ext * StorageUnit_tech_capacity_weight, over=storage_unit) + sum(Store_e_nom_ext * Store_tech_capacity_weight, over=store) + scenario_opex: + description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight + expression: >- + sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(Store_p * Store_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + + sum(sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_shut_down * Generator_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) constraints: Generator_fix_p_lower: description: "`Generator-fix-p-lower` — a fixed generator outputs at least its minimum" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom Generator_fix_p_upper: description: "`Generator-fix-p-upper` — a fixed generator outputs at most what is available" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom Link_fix_p_lower: description: "`Link-fix-p-lower` — a fixed link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - where: not Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: not Link_p_nom_extendable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom Link_fix_p_upper: description: "`Link-fix-p-upper` — a fixed link carries at most its nominal power" - dims: [snapshot, link] - where: not Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: not Link_p_nom_extendable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom Generator_ext_p_lower: description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext Generator_ext_p_upper: description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND not Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext Generator_ext_p_nom_lower: description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" @@ -750,23 +854,23 @@ constraints: expression: Generator_p_nom_ext == Generator_p_nom_set Generator_e_sum_min: description: "`Generator-e_sum_min` — energy over the horizon is at least its floor; a floor of minus infinity is no row" - dims: [generator] + dims: [scenario, generator] where: Generator_e_sum_min expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) >= Generator_e_sum_min Generator_e_sum_max: description: "`Generator-e_sum_max` — energy over the horizon is at most its budget; a budget of infinity is no row" - dims: [generator] + dims: [scenario, generator] where: Generator_e_sum_max expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) <= Generator_e_sum_max Link_ext_p_lower: description: "`Link-ext-p-lower` — an extendable link carries at least its minimum of the chosen build, negative for the other way" - dims: [snapshot, link] - where: Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: Link_p_nom_extendable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom_ext Link_ext_p_upper: description: "`Link-ext-p-upper` — an extendable link carries at most the chosen build" - dims: [snapshot, link] - where: Link_p_nom_extendable + dims: [scenario, snapshot, link] + where: Link_p_nom_extendable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom_ext Link_ext_p_nom_lower: description: "`Link-ext-p_nom-lower` — the chosen build is at least its floor" @@ -785,83 +889,83 @@ constraints: expression: Link_p_nom_ext == Link_p_nom_set StorageUnit_fix_p_dispatch_lower: description: "`StorageUnit-fix-p_dispatch-lower` — dispatch is non-negative" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch >= 0 StorageUnit_fix_p_dispatch_upper: description: "`StorageUnit-fix-p_dispatch-upper` — a fixed unit dispatches at most its nominal power" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch <= StorageUnit_p_max_pu * StorageUnit_p_nom StorageUnit_fix_p_store_lower: description: "`StorageUnit-fix-p_store-lower` — storing is non-negative" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store >= 0 StorageUnit_fix_p_store_upper: description: >- `StorageUnit-fix-p_store-upper` — a fixed unit stores at most its nominal power, the minimum-per-unit column carrying that cap negated - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store <= -StorageUnit_p_min_pu * StorageUnit_p_nom StorageUnit_fix_state_of_charge_lower: description: "`StorageUnit-fix-state_of_charge-lower` — charge is non-negative" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge >= 0 StorageUnit_fix_state_of_charge_upper: description: "`StorageUnit-fix-state_of_charge-upper` — a fixed unit holds at most its hours at nominal power" - dims: [snapshot, storage_unit] - where: not StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: not StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge <= StorageUnit_max_hours * StorageUnit_p_nom Generator_com_p_lower: description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing" - dims: [snapshot, generator] - where: Generator_committable AND not Generator_p_nom_extendable + dims: [scenario, snapshot, generator] + where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * Generator_status Generator_com_p_upper: description: "`Generator-com-p-upper` — a committed unit outputs at most what is available; off, at most nothing" - dims: [snapshot, generator] - where: Generator_committable AND not Generator_p_nom_extendable + dims: [scenario, snapshot, generator] + where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * Generator_status Generator_com_transition_start_up: description: "`Generator-com-transition-start-up` — turning on is a start, counted against the state the unit carried into the snapshot" - dims: [snapshot, generator] - where: Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_active expression: Generator_start_up >= Generator_status - Generator_previous_status Generator_com_transition_shut_down: description: "`Generator-com-transition-shut-down` — turning off is a stop, counted against the state the unit carried into the snapshot" - dims: [snapshot, generator] - where: Generator_committable + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_active expression: Generator_shut_down >= Generator_previous_status - Generator_status Generator_com_up_time: description: >- `Generator-com-up-time` — a unit started within its own minimum up time is still on. The first snapshot's share of the window is the brought-in up time's, which the must-stay-up mask carries - dims: [snapshot, generator] - where: Generator_committable AND Generator_min_up_time > 0 AND position(snapshot) > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_min_up_time > 0 AND position(snapshot) > 0 AND Generator_active expression: sum_back(Generator_start_up, along=snapshot, window=Generator_min_up_time) <= Generator_status Generator_com_down_time: description: "`Generator-com-down-time` — a unit stopped within its own minimum down time is still off" - dims: [snapshot, generator] - where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 AND Generator_active expression: sum_back(Generator_shut_down, along=snapshot, window=Generator_min_down_time) <= 1 - Generator_status Generator_com_status_must_stay_up: description: "`Generator-com-status-min_up_time_must_stay_up` — a unit still serving the up time it brought in stays on" - dims: [snapshot, generator] - where: Generator_committable AND Generator_must_stay_up + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_must_stay_up AND Generator_active expression: Generator_status == 1 Generator_p_ramp_limit_up_run_big_m: description: >- `Generator-p-ramp_limit_up-run-bigM` — a committed extendable unit raises output no faster than its limit of the chosen build; the big M releases the row in the snapshot it turns on - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_ramp_limit_up * Generator_p_nom_ext @@ -871,10 +975,10 @@ constraints: `Generator-p-ramp_limit_up-start-bigM` — in the snapshot it turns on, a committed extendable unit ramps no further than its start-up ramp of the chosen build; the big M releases the row everywhere else - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_ramp_limit_start_up * Generator_p_nom_ext @@ -884,10 +988,10 @@ constraints: `Generator-p-ramp_limit_down-run-bigM` — a committed extendable unit lowers output no faster than its limit of the chosen build; the big M releases the row in the snapshot it turns off - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_ramp_limit_down * Generator_p_nom_ext @@ -897,10 +1001,10 @@ constraints: `Generator-p-ramp_limit_down-shut-bigM` — in the snapshot it turns off, a committed extendable unit ramps no further than its shut-down ramp of the chosen build; the big M releases the row everywhere else - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down - AND (position(snapshot) > 0 OR Generator_status_initial == 0) + AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_ramp_limit_shut_down * Generator_p_nom_ext @@ -914,20 +1018,20 @@ constraints: description: >- `Generator-com-ext-p-upper-cap` — a committed extendable unit outputs at most what is available of the chosen build, whatever its status - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext Generator_com_ext_p_upper_big_m: description: "`Generator-com-ext-p-upper-bigM` — off, a unit outputs nothing; on, the big M is no bound" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: Generator_p <= Generator_big_m * Generator_status Generator_com_ext_p_lower: description: >- `Generator-com-ext-p-lower` — a committed extendable unit outputs at least its minimum of the chosen build; off, the big M releases the row - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: >- Generator_p >= Generator_p_min_pu * Generator_p_nom_ext @@ -937,83 +1041,83 @@ constraints: `Generator-com-ext-p-lower-nonneg` — where no minimum-per-unit is negative, output is also plainly non-negative, a row the big-M lower cannot assert while the unit is off - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_committable AND Generator_p_nom_extendable - AND Generator_p_min_pu_nonneg AND NOT (Generator_p_nom_mod > 0) + AND Generator_p_min_pu_nonneg AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: Generator_p >= 0 Generator_com_mod_p_lower: description: >- `Generator-com-mod-p-lower` — a committed modular unit outputs at least its minimum of one module, whether the build is fixed or a decision - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_mod * Generator_status Generator_com_mod_p_upper: description: >- `Generator-com-mod-p-upper` — a committed modular unit outputs at most one module's share, whether the build is fixed or a decision - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_mod * Generator_status Generator_status_p_fixed_upper: description: >- `Generator-status-p-fixed-upper` — a status is at most the modules in place, an explicit row as PyPSA writes it: one where the build is not modular, and the fixed build's whole count of modules where it is - dims: [snapshot, generator] - where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) AND Generator_active expression: Generator_status <= Generator_modules_installed Generator_start_up_p_fixed_upper: description: >- `Generator-start_up-p-fixed-upper` — a start is at most the modules in place, an explicit row as PyPSA writes it: one where the build is not modular, and the fixed build's whole count of modules where it is - dims: [snapshot, generator] - where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) AND Generator_active expression: Generator_start_up <= Generator_modules_installed Generator_shut_down_p_fixed_upper: description: >- `Generator-shut_down-p-fixed-upper` — a stop is at most the modules in place, an explicit row as PyPSA writes it: one where the build is not modular, and the fixed build's whole count of modules where it is - dims: [snapshot, generator] - where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) + dims: [scenario, snapshot, generator] + where: Generator_committable AND NOT (Generator_p_nom_extendable AND Generator_p_nom_mod > 0) AND Generator_active expression: Generator_shut_down <= Generator_modules_installed Generator_status_p_nom_variable_upper: description: "`Generator-status-p_nom-variable-upper` — a modular unit is on only where a module is built" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_status <= Generator_n_mod Generator_start_up_p_nom_variable_upper: description: "`Generator-start_up-p_nom-variable-upper` — a modular unit starts only where a module is built" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_start_up <= Generator_n_mod Generator_shut_down_p_nom_variable_upper: description: "`Generator-shut_down-p_nom-variable-upper` — a modular unit stops only where a module is built" - dims: [snapshot, generator] - where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_shut_down <= Generator_n_mod Line_fix_s_lower: description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating" - dims: [snapshot, line] - where: not Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: not Line_s_nom_extendable AND Line_active expression: Line_s >= -Line_s_max_pu * Line_s_nom Line_fix_s_upper: description: "`Line-fix-s-upper` — a fixed line carries at most its rating" - dims: [snapshot, line] - where: not Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: not Line_s_nom_extendable AND Line_active expression: Line_s <= Line_s_max_pu * Line_s_nom Line_ext_s_lower: description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: Line_s_nom_extendable AND Line_active expression: Line_s >= -Line_s_max_pu * Line_s_nom_ext Line_ext_s_upper: description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable + dims: [scenario, snapshot, line] + where: Line_s_nom_extendable AND Line_active expression: Line_s <= Line_s_max_pu * Line_s_nom_ext Line_ext_s_nom_lower: description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor" @@ -1032,15 +1136,15 @@ constraints: expression: Line_s_nom_ext == Line_s_nom_set Line_s_set: description: "`Line-s_set` — flow pinned to the given schedule, wherever one is given" - dims: [snapshot, line] - where: Line_s_set + dims: [scenario, snapshot, line] + where: Line_s_set AND Line_active expression: Line_s == Line_s_set Kirchhoff_Voltage_Law: description: >- `Kirchhoff-Voltage-Law` — around every independent cycle the impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport - dims: [snapshot, cycle] + dims: [scenario, snapshot, cycle] expression: sum(Line_s * Line_cycle_weight, over=line) == 0 Generator_p_ramp_limit_up: description: >- @@ -1049,11 +1153,11 @@ constraints: start-up ramp in the snapshot it turns on. A unit that came into the horizon running brought an unknown output, so it carries no row at the first snapshot — nor does any unit a big M releases instead - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_ramp_limit_up AND NOT (Generator_committable AND Generator_p_nom_extendable) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) + AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance Generator_p_ramp_limit_down: description: >- @@ -1062,56 +1166,56 @@ constraints: shut-down ramp in the snapshot it turns off. A unit that came into the horizon running brought an unknown output, so it carries no row at the first snapshot — nor does any unit a big M releases instead - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] where: >- Generator_ramp_limit_down AND NOT (Generator_committable AND Generator_p_nom_extendable) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) + AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance Link_p_ramp_limit_up: description: >- `Link-p-ramp_limit_up` — a link raises flow no faster than its limit of the build. The translated term vacates the first snapshot, where a plain optimize builds no row either - dims: [snapshot, link] - where: Link_ramp_limit_up + dims: [scenario, snapshot, link] + where: Link_ramp_limit_up AND Link_active expression: Link_p - shift(Link_p, along=snapshot, offset=1) <= Link_ramp_limit_up * Link_p_nom_effective Link_p_ramp_limit_down: description: "`Link-p-ramp_limit_down` — a link lowers flow no faster than its limit of the build" - dims: [snapshot, link] - where: Link_ramp_limit_down + dims: [scenario, snapshot, link] + where: Link_ramp_limit_down AND Link_active expression: shift(Link_p, along=snapshot, offset=1) - Link_p <= Link_ramp_limit_down * Link_p_nom_effective StorageUnit_ext_p_dispatch_lower: description: "`StorageUnit-ext-p_dispatch-lower` — dispatch is non-negative" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch >= 0 StorageUnit_ext_p_dispatch_upper: description: "`StorageUnit-ext-p_dispatch-upper` — an extendable unit dispatches at most the chosen build" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_dispatch <= StorageUnit_p_max_pu * StorageUnit_p_nom_ext StorageUnit_ext_p_store_lower: description: "`StorageUnit-ext-p_store-lower` — storing is non-negative" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store >= 0 StorageUnit_ext_p_store_upper: description: >- `StorageUnit-ext-p_store-upper` — an extendable unit stores at most the chosen build, the minimum-per-unit column carrying that cap negated - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_p_store <= -StorageUnit_p_min_pu * StorageUnit_p_nom_ext StorageUnit_ext_state_of_charge_lower: description: "`StorageUnit-ext-state_of_charge-lower` — charge is non-negative" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge >= 0 StorageUnit_ext_state_of_charge_upper: description: "`StorageUnit-ext-state_of_charge-upper` — an extendable unit holds at most its hours at the chosen build" - dims: [snapshot, storage_unit] - where: StorageUnit_p_nom_extendable + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge <= StorageUnit_max_hours * StorageUnit_p_nom_ext StorageUnit_ext_p_nom_lower: description: "`StorageUnit-ext-p_nom-lower` — the chosen build is at least its floor" @@ -1133,7 +1237,8 @@ constraints: `StorageUnit-energy_balance` — the charge carried in, plus what is stored after its efficiency, less what dispatch draws down before its own, plus inflow not spilled - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_active expression: >- StorageUnit_state_of_charge == StorageUnit_charge_carried_in @@ -1142,23 +1247,23 @@ constraints: + (StorageUnit_inflow - StorageUnit_spill) * snapshot_weightings_stores Store_fix_e_lower: description: "`Store-fix-e-lower` — a fixed store holds at least its floor" - dims: [snapshot, store] - where: not Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: not Store_e_nom_extendable AND Store_active expression: Store_e >= Store_e_min_pu * Store_e_nom Store_fix_e_upper: description: "`Store-fix-e-upper` — a fixed store holds at most its nominal capacity" - dims: [snapshot, store] - where: not Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: not Store_e_nom_extendable AND Store_active expression: Store_e <= Store_e_max_pu * Store_e_nom Store_ext_e_lower: description: "`Store-ext-e-lower` — an extendable store holds at least its floor of the chosen build" - dims: [snapshot, store] - where: Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: Store_e_nom_extendable AND Store_active expression: Store_e >= Store_e_min_pu * Store_e_nom_ext Store_ext_e_upper: description: "`Store-ext-e-upper` — an extendable store holds at most the chosen build" - dims: [snapshot, store] - where: Store_e_nom_extendable + dims: [scenario, snapshot, store] + where: Store_e_nom_extendable AND Store_active expression: Store_e <= Store_e_max_pu * Store_e_nom_ext Store_ext_e_nom_lower: description: "`Store-ext-e_nom-lower` — the chosen build is at least its floor" @@ -1177,64 +1282,65 @@ constraints: expression: Store_e_nom_ext == Store_e_nom_set Store_energy_balance: description: "`Store-energy_balance` — the energy carried in, less what is delivered to the bus" - dims: [snapshot, store] + dims: [scenario, snapshot, store] + where: Store_active expression: >- Store_e == Store_energy_carried_in - Store_p * snapshot_weightings_stores Generator_p_set: description: "`Generator-p_set` — output pinned to the given schedule, wherever one is given" - dims: [snapshot, generator] - where: Generator_p_set + dims: [scenario, snapshot, generator] + where: Generator_p_set AND Generator_active expression: Generator_p == Generator_p_set Link_p_set: description: "`Link-p_set` — flow pinned to the given schedule, wherever one is given" - dims: [snapshot, link] - where: Link_p_set + dims: [scenario, snapshot, link] + where: Link_p_set AND Link_active expression: Link_p == Link_p_set StorageUnit_p_set: description: "`StorageUnit-p_set` — net dispatch pinned to the given schedule, wherever one is given" - dims: [snapshot, storage_unit] - where: StorageUnit_p_set + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_set AND StorageUnit_active expression: StorageUnit_p_dispatch - StorageUnit_p_store == StorageUnit_p_set StorageUnit_state_of_charge_set: description: "`StorageUnit-state_of_charge_set` — charge pinned to the given schedule, wherever one is given" - dims: [snapshot, storage_unit] - where: StorageUnit_state_of_charge_set + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_state_of_charge_set AND StorageUnit_active expression: StorageUnit_state_of_charge == StorageUnit_state_of_charge_set Store_e_set: description: "`Store-e_set` — energy pinned to the given schedule, wherever one is given" - dims: [snapshot, store] - where: Store_e_set + dims: [scenario, snapshot, store] + where: Store_e_set AND Store_active expression: Store_e == Store_e_set GlobalConstraint_primary_energy_ub: description: "`primary_energy` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '<=' expression: primary_energy <= GlobalConstraint_constant GlobalConstraint_primary_energy_lb: description: "`primary_energy` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '>=' expression: primary_energy >= GlobalConstraint_constant GlobalConstraint_primary_energy_eq: description: "`primary_energy` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '==' expression: primary_energy == GlobalConstraint_constant GlobalConstraint_operational_limit_ub: description: "`operational_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'operational_limit' AND GlobalConstraint_sense == '<=' expression: operational_limit <= GlobalConstraint_constant GlobalConstraint_operational_limit_lb: description: "`operational_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'operational_limit' AND GlobalConstraint_sense == '>=' expression: operational_limit >= GlobalConstraint_constant GlobalConstraint_operational_limit_eq: description: "`operational_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'operational_limit' AND GlobalConstraint_sense == '==' expression: operational_limit == GlobalConstraint_constant GlobalConstraint_transmission_volume_expansion_limit_ub: @@ -1290,7 +1396,7 @@ constraints: load there. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. - dims: [snapshot, bus] + dims: [scenario, snapshot, bus] expression: >- sum(Generator_p, by=Generator_bus, over=generator, into=bus) + sum(StorageUnit_p_dispatch - StorageUnit_p_store, by=StorageUnit_bus, over=storage_unit, into=bus) @@ -1300,23 +1406,35 @@ constraints: - sum(Line_s, by=Line_bus0, over=line, into=bus) + sum(Line_s, by=Line_bus1, over=line, into=bus) == sum(Load_p_set, by=Load_bus, over=load, into=bus) + Carrier_growth_limit: + description: >- + `Carrier-growth_limit` — what a carrier adds in a period, counting each build in the first + period it stands in, is at most its allowance plus a share of what it added the period before; + the first period has no predecessor, so `edge=0` leaves it the bare allowance + dims: [carrier, period] + where: Carrier_max_growth + expression: >- + sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) + - shift(sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier), along=period, offset=1, edge=0) + * Carrier_max_relative_growth + <= Carrier_max_growth + CVaR_excess: + description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" + dims: [scenario] + expression: CVaR_a - scenario_opex + CVaR_theta >= 0 + CVaR_def: + description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" + dims: [] + expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR objective: sense: minimize - description: operating cost, each snapshot weighted by the hours it stands for + description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective) - + sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective) - + sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective) - + sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective) - + sum(Store_p * Store_marginal_cost * snapshot_weightings_objective) - + sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective) - + sum(Generator_p_nom_ext * Generator_capital_cost) - + sum(Link_p_nom_ext * Link_capital_cost) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost) - + sum(Store_e_nom_ext * Store_capital_cost) - + sum(Line_s_nom_ext * Line_capital_cost) - + sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective) - + sum(Generator_start_up * Generator_start_up_cost) - + sum(Generator_shut_down * Generator_shut_down_cost) + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + CVaR_omega * CVaR diff --git a/examples/pypsa_multi_period.yaml b/examples/pypsa_multi_period.yaml deleted file mode 100644 index 97a31c34..00000000 --- a/examples/pypsa_multi_period.yaml +++ /dev/null @@ -1,229 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -description: >- - The multi-period class of a plain `n.optimize()`: - `multi_investment_periods`, stated on rungs 1 and 3 in a file of its own. - A snapshot belongs to an investment period, an asset stands in the periods - its build year and lifetime span, and capacity is paid once per period it - stands in, each period weighted; a carrier may grow only so much per - period. Which snapshots an asset is active in is data prep, because a - `where` reaches only the frame's own dimensions. A dimension a run may not - have cannot ride on `examples/pypsa.yaml`, so this class lives here. - -dimensions: - snapshot: - description: dispatch periods, positions across every investment period - dtype: datetime - period: - description: investment periods — PyPSA's `investment_periods` - dtype: int - bus: - description: network nodes - generator: - description: generating units, each on one bus - link: - description: controllable connections, each from one bus to the buses it delivers to - link_output: - description: >- - a link's output ports, one label per port a link declares — PyPSA's - `bus1`, `bus2`, … columns read long, so a link of any number of output - ports is one term in the balance, data prep - load: - description: demands, each on one bus - carrier: - description: energy carriers, what a growth limit is set per - -parameters: - snapshot_weightings_objective: - description: PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost - dims: [snapshot] - period_weight_objective: - description: PyPSA's `investment_period_weightings.objective` — what a period's cost weighs - dims: [period] - Generator_active: - description: whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep - dims: [snapshot, generator] - dtype: bool - Generator_capital_weight: - description: the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep - dims: [generator] - Generator_first_active: - description: one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep - dims: [period, generator] - Carrier_max_growth: - description: most capacity of a carrier that may be added in a period; no value means no limit - dims: [carrier] - Carrier_max_relative_growth: - description: share of the previous period's additions that may be added on top - dims: [carrier] - Generator_p_nom: - description: nominal power - dims: [generator] - Generator_p_nom_extendable: - description: whether the nominal power is a decision - dims: [generator] - dtype: bool - Generator_p_nom_min: - description: least nominal power an extendable generator may be built at - dims: [generator] - Generator_p_nom_max: - description: most nominal power an extendable generator may be built at - dims: [generator] - Generator_capital_cost: - description: cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [generator] - Generator_p_min_pu: - description: least output, per unit of nominal power - dims: [snapshot, generator] - Generator_p_max_pu: - description: most output, per unit of nominal power — an availability profile - dims: [snapshot, generator] - Generator_marginal_cost: - description: cost of one unit of output - dims: [snapshot, generator] - Link_p_nom: - description: nominal power - dims: [link] - Link_p_min_pu: - description: least flow, per unit of nominal power — negative for a link that carries both ways - dims: [snapshot, link] - Link_p_max_pu: - description: most flow, per unit of nominal power - dims: [snapshot, link] - Link_efficiency: - description: >- - share of the flow that arrives at an output port, PyPSA's `efficiency`, - `efficiency2`, … read long — negative where that port consumes rather - than delivers - dims: [link_output] - Link_marginal_cost: - description: cost of one unit of flow - dims: [snapshot, link] - Load_p_set: - description: demand - dims: [snapshot, load] - -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Generator_carrier: - description: the carrier a generator converts from - key: generator - values: carrier - Generator_bus: - description: the bus a generator sits on - key: generator - values: bus - Link_bus0: - description: the bus a link leaves - key: link - values: bus - Link_output_link: - description: the link an output port belongs to - key: link_output - values: link - Link_output_bus: - description: >- - the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. - A link of three output ports is three labels here rather than a third - relation, so the file states any number of them - key: link_output - values: bus - Load_bus: - description: the bus a load sits on - key: load - values: bus - -variables: - Generator_p: - description: "`Generator-p` — output of a generator in a snapshot" - dims: [snapshot, generator] - where: Generator_active - Link_p: - description: >- - `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a - positive value withdraws there and injects at every bus the link's - output ports deliver to - dims: [snapshot, link] - Generator_p_nom_ext: - description: >- - `Generator-p_nom` — nominal power where it is a decision; the parameter - of the same PyPSA name carries the fixed regime - dims: [generator] - where: Generator_p_nom_extendable - -constraints: - Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom - Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [snapshot, generator] - where: not Generator_p_nom_extendable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom - Generator_ext_p_lower: - description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext - Generator_ext_p_upper: - description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" - dims: [snapshot, generator] - where: Generator_p_nom_extendable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext - Generator_ext_p_nom_lower: - description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [generator] - where: Generator_p_nom_extendable - expression: Generator_p_nom_ext >= Generator_p_nom_min - Generator_ext_p_nom_upper: - description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [generator] - where: Generator_p_nom_extendable AND Generator_p_nom_max - expression: Generator_p_nom_ext <= Generator_p_nom_max - Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom - Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom - Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, less what the links - take away, plus what arrives over them after losses, meets the load - there - dims: [snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) - Carrier_growth_limit: - description: >- - `Carrier-growth_limit` — what a carrier adds in a period, counting each - build in the first period it stands in, is at most its allowance plus a - share of what it added the period before; the first period has no - predecessor, so `edge=0` leaves it the bare allowance - dims: [carrier, period] - where: Carrier_max_growth - expression: >- - sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) - - shift(sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier), along=period, offset=1, edge=0) - * Carrier_max_relative_growth - <= Carrier_max_growth - -objective: - sense: minimize - description: operating cost by weighted snapshot and weighted period, and capacity once per period it stands in - expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot)) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot)) - + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) diff --git a/examples/pypsa_stochastic.yaml b/examples/pypsa_stochastic.yaml deleted file mode 100644 index a5ebe4fb..00000000 --- a/examples/pypsa_stochastic.yaml +++ /dev/null @@ -1,220 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -description: >- - The two-stage class of a plain `n.optimize()`: a network with scenarios, - stated on rung 1's transport and rung 3's expansion in a file of its own. - Everything over a snapshot spans a scenario as well; capacity does not — - it is chosen once, before the future is known — and the cost is the - expectation over the scenarios' weights. With a risk preference PyPSA adds - the CVaR rows: an excess per scenario and the tail's average, blended into - the objective. A dimension a run may not have cannot ride on - `examples/pypsa.yaml`, so this class lives here. - -dimensions: - scenario: - description: the futures dispatch is chosen in, each with a weight - snapshot: - description: dispatch periods - dtype: datetime - bus: - description: network nodes - generator: - description: generating units, each on one bus - link: - description: controllable connections, each from one bus to the buses it delivers to - link_output: - description: >- - a link's output ports, one label per port a link declares — PyPSA's - `bus1`, `bus2`, … columns read long, so a link of any number of output - ports is one term in the balance, data prep - load: - description: demands, each on one bus - -parameters: - scenario_weight: - description: PyPSA's `scenario_weightings.weight` — the probability of a future - dims: [scenario] - CVaR_omega: - description: PyPSA's `risk_preference['omega']` — the share of the operating cost priced at the tail rather than in expectation - dims: [] - CVaR_inv_tail: - description: PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor - dims: [] - snapshot_weightings_objective: - description: PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost - dims: [snapshot] - Generator_p_nom: - description: nominal power - dims: [generator] - Generator_p_nom_extendable: - description: whether the nominal power is a decision - dims: [generator] - dtype: bool - Generator_p_nom_min: - description: least nominal power an extendable generator may be built at - dims: [generator] - Generator_p_nom_max: - description: most nominal power an extendable generator may be built at - dims: [generator] - Generator_capital_cost: - description: cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [generator] - Generator_p_min_pu: - description: least output, per unit of nominal power - dims: [snapshot, generator] - Generator_p_max_pu: - description: most output, per unit of nominal power — an availability profile - dims: [scenario, snapshot, generator] - Generator_marginal_cost: - description: cost of one unit of output - dims: [snapshot, generator] - Link_p_nom: - description: nominal power - dims: [link] - Link_p_min_pu: - description: least flow, per unit of nominal power — negative for a link that carries both ways - dims: [snapshot, link] - Link_p_max_pu: - description: most flow, per unit of nominal power - dims: [snapshot, link] - Link_efficiency: - description: >- - share of the flow that arrives at an output port, PyPSA's `efficiency`, - `efficiency2`, … read long — negative where that port consumes rather - than delivers - dims: [link_output] - Link_marginal_cost: - description: cost of one unit of flow - dims: [snapshot, link] - Load_p_set: - description: demand - dims: [scenario, snapshot, load] - -relations: - Generator_bus: - description: the bus a generator sits on - key: generator - values: bus - Link_bus0: - description: the bus a link leaves - key: link - values: bus - Link_output_link: - description: the link an output port belongs to - key: link_output - values: link - Link_output_bus: - description: >- - the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. - A link of three output ports is three labels here rather than a third - relation, so the file states any number of them - key: link_output - values: bus - Load_bus: - description: the bus a load sits on - key: load - values: bus - -variables: - Generator_p: - description: "`Generator-p` — output of a generator in a snapshot" - dims: [scenario, snapshot, generator] - Link_p: - description: >- - `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a - positive value withdraws there and injects at every bus the link's - output ports deliver to - dims: [scenario, snapshot, link] - Generator_p_nom_ext: - description: >- - `Generator-p_nom` — nominal power where it is a decision; the parameter - of the same PyPSA name carries the fixed regime - dims: [generator] - where: Generator_p_nom_extendable - CVaR_a: - description: "`CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not" - dims: [scenario] - bounds: - lower: 0 - CVaR_theta: - description: "`CVaR-theta` — where the tail starts, the value at risk" - dims: [] - CVaR: - description: "`CVaR` — the tail's average cost, what the objective prices at `omega`" - dims: [] - -expressions: - scenario_opex: - description: what a future costs to run — the operating terms, before their weight - expression: >- - sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective, over=generator), over=snapshot) - + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective, over=link), over=snapshot) - -constraints: - Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [scenario, snapshot, generator] - where: not Generator_p_nom_extendable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom - Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [scenario, snapshot, generator] - where: not Generator_p_nom_extendable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom - Generator_ext_p_lower: - description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" - dims: [scenario, snapshot, generator] - where: Generator_p_nom_extendable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext - Generator_ext_p_upper: - description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" - dims: [scenario, snapshot, generator] - where: Generator_p_nom_extendable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext - Generator_ext_p_nom_lower: - description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [generator] - where: Generator_p_nom_extendable - expression: Generator_p_nom_ext >= Generator_p_nom_min - Generator_ext_p_nom_upper: - description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [generator] - where: Generator_p_nom_extendable AND Generator_p_nom_max - expression: Generator_p_nom_ext <= Generator_p_nom_max - Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [scenario, snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom - Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [scenario, snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom - Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, less what the links - take away, plus what arrives over them after losses, meets the load - there - dims: [scenario, snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) - CVaR_excess: - description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" - dims: [scenario] - expression: CVaR_a - scenario_opex + CVaR_theta >= 0 - CVaR_def: - description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" - dims: [] - expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR - -objective: - sense: minimize - description: capacity once, operation in expectation, and a share of it at the tail - expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) - + CVaR_omega * CVaR diff --git a/examples/references/pypsa/rung_14_stochastic.py b/examples/references/pypsa/rung_14_stochastic.py index f93c4ef5..24ae3f79 100644 --- a/examples/references/pypsa/rung_14_stochastic.py +++ b/examples/references/pypsa/rung_14_stochastic.py @@ -2,14 +2,12 @@ # # SPDX-License-Identifier: MIT -"""Rung 14: two futures and a risk preference — capacity chosen once, dispatch per scenario, stated by `pypsa_stochastic.yaml`.""" +"""Rung 14: two futures and a risk preference — capacity chosen once, dispatch per scenario.""" from __future__ import annotations import spine -MODEL = 'pypsa_stochastic.yaml' - def build(): """The spine plus an extendable wind unit whose availability and the south's load differ between a calm and a stormy future.""" diff --git a/examples/references/pypsa/rung_15_multi_period.py b/examples/references/pypsa/rung_15_multi_period.py index 220ae1ee..4d5d3981 100644 --- a/examples/references/pypsa/rung_15_multi_period.py +++ b/examples/references/pypsa/rung_15_multi_period.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -"""Rung 15: two investment periods — build years, lifetimes, period weights and a carrier's growth limit, stated by `pypsa_multi_period.yaml`.""" +"""Rung 15: two investment periods — build years, lifetimes, period weights and a carrier's growth limit.""" from __future__ import annotations @@ -10,7 +10,6 @@ import pandas as pd -MODEL = 'pypsa_multi_period.yaml' OPTIMIZE = {'multi_investment_periods': True} diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 5be16db1..9c871565 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -16,6 +16,9 @@ dimensions: store: { index: v, set: '\mathcal{V}' } line: { index: k, set: '\mathcal{K}' } cycle: { index: c, set: '\mathcal{C}' } + scenario: { index: '\xi', set: '\Xi' } + period: { index: y, set: '\mathcal{Y}' } + carrier: { index: i, set: '\mathcal{I}' } names: snapshot_weightings_objective: '\mathrm{w}' Generator_p: p @@ -153,3 +156,23 @@ names: Store_marginal_cost: '\mathrm{c}^{q}' Store_marginal_cost_storage: '\mathrm{c}^{e}' Store_e_set: '\mathrm{e}^{\mathrm{set}}' + scenario_weight: '\pi' + CVaR_omega: '\omega' + CVaR_inv_tail: '\mathrm{v}' + CVaR_a: a + CVaR_theta: '\theta' + CVaR: "CVaR" + period_weight_objective: '\mathrm{w}^{y}' + Generator_active: '\mathrm{on}' + Link_active: '\mathrm{on}^{f}' + StorageUnit_active: '\mathrm{on}^{h}' + Store_active: '\mathrm{on}^{e}' + Line_active: '\mathrm{on}^{s}' + Generator_capital_weight: '\mathrm{W}' + Link_capital_weight: '\mathrm{W}^{f}' + StorageUnit_capital_weight: '\mathrm{W}^{h}' + Store_capital_weight: '\mathrm{W}^{e}' + Line_capital_weight: '\mathrm{W}^{s}' + Generator_first_active: '\mathrm{new}' + Carrier_max_growth: '\overline{\Delta}' + Carrier_max_relative_growth: '\mathrm{r}' diff --git a/examples/symbols/pypsa_multi_period.yaml b/examples/symbols/pypsa_multi_period.yaml deleted file mode 100644 index 8fa00219..00000000 --- a/examples/symbols/pypsa_multi_period.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -# How `examples/pypsa_multi_period.yaml` prints — the transport and expansion symbols of `pypsa.yaml`, a period, a carrier. -notation: latex -dimensions: - snapshot: { index: t, set: '\mathcal{T}' } - bus: { index: n, set: '\mathcal{N}' } - generator: { index: g, set: '\mathcal{G}' } - link: { index: l, set: '\mathcal{L}' } - link_output: { index: o, set: '\mathcal{O}' } - load: { index: d, set: '\mathcal{D}' } - period: { index: y, set: '\mathcal{Y}' } - carrier: { index: c, set: '\mathcal{C}' } -names: - snapshot_weightings_objective: '\mathrm{w}' - Generator_p: p - Generator_p_nom: '\mathrm{p}^{\mathrm{nom}}' - Generator_p_nom_extendable: '\mathrm{ext}' - Generator_p_min_pu: '\underline{\mathrm{p}}' - Generator_p_max_pu: '\overline{\mathrm{p}}' - Generator_marginal_cost: '\mathrm{c}' - Link_p: f - Link_p_nom: '\mathrm{f}^{\mathrm{nom}}' - Link_p_min_pu: '\underline{\mathrm{f}}' - Link_p_max_pu: '\overline{\mathrm{f}}' - Link_efficiency: '\eta' - Link_marginal_cost: '\mathrm{c}^{f}' - Load_p_set: '\mathrm{load}' - Generator_p_nom_ext: P - Generator_p_nom_min: '\underline{\mathrm{p}}^{\mathrm{nom}}' - Generator_p_nom_max: '\overline{\mathrm{p}}^{\mathrm{nom}}' - Generator_capital_cost: '\mathrm{c}^{\mathrm{cap}}' - period_weight_objective: '\mathrm{w}^{y}' - Generator_active: '\mathrm{on}' - Generator_capital_weight: '\mathrm{W}' - Generator_first_active: '\mathrm{new}' - Carrier_max_growth: '\overline{\Delta}' - Carrier_max_relative_growth: '\mathrm{r}' diff --git a/examples/symbols/pypsa_stochastic.yaml b/examples/symbols/pypsa_stochastic.yaml deleted file mode 100644 index 2e64e671..00000000 --- a/examples/symbols/pypsa_stochastic.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -# How `examples/pypsa_stochastic.yaml` prints — the transport and expansion symbols of `pypsa.yaml`, a scenario, and the tail. -notation: latex -dimensions: - snapshot: { index: t, set: '\mathcal{T}' } - bus: { index: n, set: '\mathcal{N}' } - generator: { index: g, set: '\mathcal{G}' } - link: { index: l, set: '\mathcal{L}' } - link_output: { index: o, set: '\mathcal{O}' } - load: { index: d, set: '\mathcal{D}' } - scenario: { index: s, set: '\mathcal{S}' } -names: - snapshot_weightings_objective: '\mathrm{w}' - Generator_p: p - Generator_p_nom: '\mathrm{p}^{\mathrm{nom}}' - Generator_p_nom_extendable: '\mathrm{ext}' - Generator_p_min_pu: '\underline{\mathrm{p}}' - Generator_p_max_pu: '\overline{\mathrm{p}}' - Generator_marginal_cost: '\mathrm{c}' - Link_p: f - Link_p_nom: '\mathrm{f}^{\mathrm{nom}}' - Link_p_min_pu: '\underline{\mathrm{f}}' - Link_p_max_pu: '\overline{\mathrm{f}}' - Link_efficiency: '\eta' - Link_marginal_cost: '\mathrm{c}^{f}' - Load_p_set: '\mathrm{load}' - Generator_p_nom_ext: P - Generator_p_nom_min: '\underline{\mathrm{p}}^{\mathrm{nom}}' - Generator_p_nom_max: '\overline{\mathrm{p}}^{\mathrm{nom}}' - Generator_capital_cost: '\mathrm{c}^{\mathrm{cap}}' - scenario_weight: '\pi' - CVaR_omega: '\omega' - CVaR_inv_tail: '\mathrm{v}' - CVaR_a: a - CVaR_theta: '\theta' - CVaR: "CVaR" diff --git a/mkdocs.yml b/mkdocs.yml index ccc32aff..33d2c00b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,8 +66,6 @@ nav: - PyPSA, the quadratic class: examples/pypsa_quadratic.md - PyPSA, the relaxed commitment: examples/pypsa_linearized_uc.md - PyPSA, the lossy lines: examples/pypsa_losses.md - - PyPSA, the two-stage class: examples/pypsa_stochastic.md - - PyPSA, the multi-period class: examples/pypsa_multi_period.md # Everything a reader does not need in order to write a model: the design # arguments, how to contribute, what changed. - About: diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml new file mode 100644 index 00000000..2c6ac7f8 --- /dev/null +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -0,0 +1,607 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +# The shape of the standard PyPSA model, frozen before examples/pypsa.yaml +# became the unified superset. tests/test_pypsa_collapses.py asserts the +# superset, fed one scenario and all-active periods, reduces to exactly this. + +constraints: + Bus_nodal_balance: + - snapshot + - bus + Generator_com_down_time: + - snapshot + - generator + Generator_com_ext_p_lower: + - snapshot + - generator + Generator_com_ext_p_lower_nonneg: + - snapshot + - generator + Generator_com_ext_p_upper_big_m: + - snapshot + - generator + Generator_com_ext_p_upper_cap: + - snapshot + - generator + Generator_com_mod_p_lower: + - snapshot + - generator + Generator_com_mod_p_upper: + - snapshot + - generator + Generator_com_p_lower: + - snapshot + - generator + Generator_com_p_upper: + - snapshot + - generator + Generator_com_status_must_stay_up: + - snapshot + - generator + Generator_com_transition_shut_down: + - snapshot + - generator + Generator_com_transition_start_up: + - snapshot + - generator + Generator_com_up_time: + - snapshot + - generator + Generator_e_sum_max: + - generator + Generator_e_sum_min: + - generator + Generator_ext_p_lower: + - snapshot + - generator + Generator_ext_p_nom_lower: + - generator + Generator_ext_p_nom_upper: + - generator + Generator_ext_p_upper: + - snapshot + - generator + Generator_fix_p_lower: + - snapshot + - generator + Generator_fix_p_upper: + - snapshot + - generator + Generator_p_nom_modularity: + - generator + Generator_p_nom_set: + - generator + Generator_p_ramp_limit_down: + - snapshot + - generator + Generator_p_ramp_limit_down_run_big_m: + - snapshot + - generator + Generator_p_ramp_limit_down_shut_big_m: + - snapshot + - generator + Generator_p_ramp_limit_up: + - snapshot + - generator + Generator_p_ramp_limit_up_run_big_m: + - snapshot + - generator + Generator_p_ramp_limit_up_start_big_m: + - snapshot + - generator + Generator_p_set: + - snapshot + - generator + Generator_shut_down_p_fixed_upper: + - snapshot + - generator + Generator_shut_down_p_nom_variable_upper: + - snapshot + - generator + Generator_start_up_p_fixed_upper: + - snapshot + - generator + Generator_start_up_p_nom_variable_upper: + - snapshot + - generator + Generator_status_p_fixed_upper: + - snapshot + - generator + Generator_status_p_nom_variable_upper: + - snapshot + - generator + GlobalConstraint_operational_limit_eq: + - global_constraint + GlobalConstraint_operational_limit_lb: + - global_constraint + GlobalConstraint_operational_limit_ub: + - global_constraint + GlobalConstraint_primary_energy_eq: + - global_constraint + GlobalConstraint_primary_energy_lb: + - global_constraint + GlobalConstraint_primary_energy_ub: + - global_constraint + GlobalConstraint_tech_capacity_expansion_limit_eq: + - global_constraint + GlobalConstraint_tech_capacity_expansion_limit_lb: + - global_constraint + GlobalConstraint_tech_capacity_expansion_limit_ub: + - global_constraint + GlobalConstraint_transmission_expansion_cost_limit_eq: + - global_constraint + GlobalConstraint_transmission_expansion_cost_limit_lb: + - global_constraint + GlobalConstraint_transmission_expansion_cost_limit_ub: + - global_constraint + GlobalConstraint_transmission_volume_expansion_limit_eq: + - global_constraint + GlobalConstraint_transmission_volume_expansion_limit_lb: + - global_constraint + GlobalConstraint_transmission_volume_expansion_limit_ub: + - global_constraint + Kirchhoff_Voltage_Law: + - snapshot + - cycle + Line_ext_s_lower: + - snapshot + - line + Line_ext_s_nom_lower: + - line + Line_ext_s_nom_upper: + - line + Line_ext_s_upper: + - snapshot + - line + Line_fix_s_lower: + - snapshot + - line + Line_fix_s_upper: + - snapshot + - line + Line_s_nom_set: + - line + Line_s_set: + - snapshot + - line + Link_ext_p_lower: + - snapshot + - link + Link_ext_p_nom_lower: + - link + Link_ext_p_nom_upper: + - link + Link_ext_p_upper: + - snapshot + - link + Link_fix_p_lower: + - snapshot + - link + Link_fix_p_upper: + - snapshot + - link + Link_p_nom_set: + - link + Link_p_ramp_limit_down: + - snapshot + - link + Link_p_ramp_limit_up: + - snapshot + - link + Link_p_set: + - snapshot + - link + StorageUnit_energy_balance: + - snapshot + - storage_unit + StorageUnit_ext_p_dispatch_lower: + - snapshot + - storage_unit + StorageUnit_ext_p_dispatch_upper: + - snapshot + - storage_unit + StorageUnit_ext_p_nom_lower: + - storage_unit + StorageUnit_ext_p_nom_upper: + - storage_unit + StorageUnit_ext_p_store_lower: + - snapshot + - storage_unit + StorageUnit_ext_p_store_upper: + - snapshot + - storage_unit + StorageUnit_ext_state_of_charge_lower: + - snapshot + - storage_unit + StorageUnit_ext_state_of_charge_upper: + - snapshot + - storage_unit + StorageUnit_fix_p_dispatch_lower: + - snapshot + - storage_unit + StorageUnit_fix_p_dispatch_upper: + - snapshot + - storage_unit + StorageUnit_fix_p_store_lower: + - snapshot + - storage_unit + StorageUnit_fix_p_store_upper: + - snapshot + - storage_unit + StorageUnit_fix_state_of_charge_lower: + - snapshot + - storage_unit + StorageUnit_fix_state_of_charge_upper: + - snapshot + - storage_unit + StorageUnit_p_nom_set: + - storage_unit + StorageUnit_p_set: + - snapshot + - storage_unit + StorageUnit_state_of_charge_set: + - snapshot + - storage_unit + Store_e_nom_set: + - store + Store_e_set: + - snapshot + - store + Store_energy_balance: + - snapshot + - store + Store_ext_e_lower: + - snapshot + - store + Store_ext_e_nom_lower: + - store + Store_ext_e_nom_upper: + - store + Store_ext_e_upper: + - snapshot + - store + Store_fix_e_lower: + - snapshot + - store + Store_fix_e_upper: + - snapshot + - store +expressions: +- Generator_p_nom_effective +- Generator_previous_p +- Generator_previous_status +- Generator_ramp_down_allowance +- Generator_ramp_up_allowance +- Link_output_arrival +- Link_p_nom_effective +- StorageUnit_charge_carried_in +- Store_energy_carried_in +- operational_limit +- primary_energy +- tech_capacity_expansion +- transmission_expansion_cost +- transmission_volume_expansion +parameters: + Generator_big_m: + - generator + Generator_capital_cost: + - generator + Generator_committable: + - generator + Generator_e_sum_max: + - generator + Generator_e_sum_min: + - generator + Generator_marginal_cost: + - snapshot + - generator + Generator_min_down_time: + - generator + Generator_min_up_time: + - generator + Generator_modules_installed: + - generator + Generator_must_stay_up: + - snapshot + - generator + Generator_operational_limit_weight: + - global_constraint + - generator + Generator_p_max_pu: + - snapshot + - generator + Generator_p_min_pu: + - snapshot + - generator + Generator_p_min_pu_nonneg: + - generator + Generator_p_nom: + - generator + Generator_p_nom_extendable: + - generator + Generator_p_nom_max: + - generator + Generator_p_nom_min: + - generator + Generator_p_nom_mod: + - generator + Generator_p_nom_set: + - generator + Generator_p_set: + - snapshot + - generator + Generator_primary_energy_weight: + - global_constraint + - generator + Generator_ramp_limit_down: + - generator + Generator_ramp_limit_shut_down: + - generator + Generator_ramp_limit_start_up: + - generator + Generator_ramp_limit_up: + - generator + Generator_shut_down_cost: + - generator + Generator_stand_by_cost: + - snapshot + - generator + Generator_start_up_cost: + - generator + Generator_status_initial: + - generator + Generator_tech_capacity_weight: + - global_constraint + - generator + GlobalConstraint_constant: + - global_constraint + GlobalConstraint_sense: + - global_constraint + GlobalConstraint_type: + - global_constraint + Line_capital_cost: + - line + Line_cycle_weight: + - line + - cycle + Line_expansion_cost_weight: + - global_constraint + - line + Line_s_max_pu: + - snapshot + - line + Line_s_nom: + - line + Line_s_nom_extendable: + - line + Line_s_nom_max: + - line + Line_s_nom_min: + - line + Line_s_nom_set: + - line + Line_s_set: + - snapshot + - line + Line_tech_capacity_weight: + - global_constraint + - line + Line_volume_weight: + - global_constraint + - line + Link_capital_cost: + - link + Link_efficiency: + - link_output + Link_expansion_cost_weight: + - global_constraint + - link + Link_marginal_cost: + - snapshot + - link + Link_output_cyclic_delay: + - link_output + Link_output_delay: + - link_output + Link_p_max_pu: + - snapshot + - link + Link_p_min_pu: + - snapshot + - link + Link_p_nom: + - link + Link_p_nom_extendable: + - link + Link_p_nom_max: + - link + Link_p_nom_min: + - link + Link_p_nom_set: + - link + Link_p_set: + - snapshot + - link + Link_ramp_limit_down: + - link + Link_ramp_limit_up: + - link + Link_tech_capacity_weight: + - global_constraint + - link + Link_volume_weight: + - global_constraint + - link + Load_p_set: + - snapshot + - load + StorageUnit_capital_cost: + - storage_unit + StorageUnit_cyclic_state_of_charge: + - storage_unit + StorageUnit_efficiency_dispatch: + - storage_unit + StorageUnit_efficiency_store: + - storage_unit + StorageUnit_inflow: + - snapshot + - storage_unit + StorageUnit_marginal_cost: + - snapshot + - storage_unit + StorageUnit_marginal_cost_storage: + - snapshot + - storage_unit + StorageUnit_max_hours: + - storage_unit + StorageUnit_operational_limit_weight: + - global_constraint + - storage_unit + StorageUnit_p_max_pu: + - snapshot + - storage_unit + StorageUnit_p_min_pu: + - snapshot + - storage_unit + StorageUnit_p_nom: + - storage_unit + StorageUnit_p_nom_extendable: + - storage_unit + StorageUnit_p_nom_max: + - storage_unit + StorageUnit_p_nom_min: + - storage_unit + StorageUnit_p_nom_set: + - storage_unit + StorageUnit_p_set: + - snapshot + - storage_unit + StorageUnit_primary_energy_weight: + - global_constraint + - storage_unit + StorageUnit_retention: + - snapshot + - storage_unit + StorageUnit_spill_cost: + - snapshot + - storage_unit + StorageUnit_state_of_charge_initial: + - storage_unit + StorageUnit_state_of_charge_set: + - snapshot + - storage_unit + StorageUnit_tech_capacity_weight: + - global_constraint + - storage_unit + Store_capital_cost: + - store + Store_e_cyclic: + - store + Store_e_initial: + - store + Store_e_max_pu: + - snapshot + - store + Store_e_min_pu: + - snapshot + - store + Store_e_nom: + - store + Store_e_nom_extendable: + - store + Store_e_nom_max: + - store + Store_e_nom_min: + - store + Store_e_nom_set: + - store + Store_e_set: + - snapshot + - store + Store_marginal_cost: + - snapshot + - store + Store_marginal_cost_storage: + - snapshot + - store + Store_operational_limit_weight: + - global_constraint + - store + Store_primary_energy_weight: + - global_constraint + - store + Store_retention: + - snapshot + - store + Store_tech_capacity_weight: + - global_constraint + - store + snapshot_is_last: + - snapshot + snapshot_weightings_generators: + - snapshot + snapshot_weightings_objective: + - snapshot + snapshot_weightings_stores: + - snapshot +relations: +- Generator_bus +- Line_bus0 +- Line_bus1 +- Link_bus0 +- Link_output_bus +- Link_output_link +- Load_bus +- StorageUnit_bus +- Store_bus +variables: + Generator_n_mod: + - generator + Generator_p: + - snapshot + - generator + Generator_p_nom_ext: + - generator + Generator_shut_down: + - snapshot + - generator + Generator_start_up: + - snapshot + - generator + Generator_status: + - snapshot + - generator + Line_s: + - snapshot + - line + Line_s_nom_ext: + - line + Link_p: + - snapshot + - link + Link_p_nom_ext: + - link + StorageUnit_p_dispatch: + - snapshot + - storage_unit + StorageUnit_p_nom_ext: + - storage_unit + StorageUnit_p_store: + - snapshot + - storage_unit + StorageUnit_spill: + - snapshot + - storage_unit + StorageUnit_state_of_charge: + - snapshot + - storage_unit + Store_e: + - snapshot + - store + Store_e_nom_ext: + - store + Store_p: + - snapshot + - store diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py new file mode 100644 index 00000000..8f7ef633 --- /dev/null +++ b/tests/test_pypsa_collapses.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""`examples/pypsa.yaml` is the superset that collapses to the standard PyPSA model. + +The file always declares a `scenario` axis, a mask-only `period` axis, a `carrier` +axis and the CVaR rows. Fed one scenario, one period, all-active masks and unit +weights, every addition is a no-op and the standard model returns. The repository +runs no solver, so the reduction is guarded structurally: `tests/fixtures/ +pypsa_standard_shape.yaml` freezes the standard model's names and frames, and the +only frame change a standard row may carry is a leading `scenario`. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from math_spec import to_spec +from math_spec._yaml import read_yaml +from tests.fixtures import EXAMPLES + +STANDARD = read_yaml(Path(__file__).resolve().parent / 'fixtures' / 'pypsa_standard_shape.yaml') +ALL = to_spec(str(EXAMPLES / 'pypsa.yaml')) + +#: capacity is chosen once, before the future is known, so it spans no scenario +SCENARIO_FREE_VARIABLES = { + 'Generator_n_mod', + 'Generator_p_nom_ext', + 'Link_p_nom_ext', + 'StorageUnit_p_nom_ext', + 'Store_e_nom_ext', + 'Line_s_nom_ext', +} + + +def _dims(block: Any) -> list[str]: + return list(block.dims or []) + + +def test_every_standard_name_survives(): + sections = { + 'parameters': ALL.parameters, + 'relations': ALL.relations, + 'variables': ALL.variables, + 'expressions': ALL.expressions, + 'constraints': ALL.constraints, + } + dropped = { + f'{section}.{name}' for section, present in sections.items() for name in set(STANDARD[section]) - set(present) + } + assert not dropped, f'the unified file lost {sorted(dropped)} — it must keep every standard name' + + +def test_a_variable_gains_scenario_only_when_it_is_second_stage(): + wrong = { + name: _dims(ALL.variables[name]) + for name, dims in STANDARD['variables'].items() + if _dims(ALL.variables[name]) != (dims if name in SCENARIO_FREE_VARIABLES else ['scenario', *dims]) + } + assert not wrong, f'these variables carry the wrong frame: {wrong}' + + +def test_a_standard_constraint_changes_frame_only_by_a_leading_scenario(): + wrong = { + name: _dims(ALL.constraints[name]) + for name, dims in STANDARD['constraints'].items() + if _dims(ALL.constraints[name]) not in (dims, ['scenario', *dims]) + } + assert not wrong, f'a standard row may gain only a leading scenario; these differ: {wrong}' + + +def test_only_load_carries_scenario_among_standard_parameters(): + wrong = { + name: _dims(ALL.parameters[name]) + for name, dims in STANDARD['parameters'].items() + if _dims(ALL.parameters[name]) != (['scenario', *dims] if name == 'Load_p_set' else dims) + } + assert not wrong, f'only Load_p_set gains scenario; these differ: {wrong}' + + +def test_the_extra_axes_and_rows_are_declared(): + assert {'scenario', 'period', 'carrier'} <= set(ALL.dimensions), 'the three extra axes are declared' + assert {'CVaR_a', 'CVaR_theta', 'CVaR'} <= set(ALL.variables), 'the CVaR variables are declared' + assert {'CVaR_excess', 'CVaR_def', 'Carrier_growth_limit'} <= set(ALL.constraints), ( + 'the CVaR rows and the carrier growth limit are declared' + ) + assert 'scenario_opex' in ALL.expressions, 'the per-scenario operating cost is a named expression' + + +def test_omega_blends_expectation_and_tail_in_the_objective(): + expr = ALL.objective.expression + assert all(term in expr for term in ('CVaR_omega', 'scenario_weight', 'scenario_opex', 'CVaR')), ( + 'the objective prices expected opex by scenario weight and blends the tail by omega' + ) diff --git a/tools/gallery.py b/tools/gallery.py index 14a4a1ec..de078b7a 100644 --- a/tools/gallery.py +++ b/tools/gallery.py @@ -47,8 +47,6 @@ 'pypsa_quadratic.md': ROOT / 'examples' / 'pypsa_quadratic.yaml', 'pypsa_linearized_uc.md': ROOT / 'examples' / 'pypsa_linearized_uc.yaml', 'pypsa_losses.md': ROOT / 'examples' / 'pypsa_losses.yaml', - 'pypsa_stochastic.md': ROOT / 'examples' / 'pypsa_stochastic.yaml', - 'pypsa_multi_period.md': ROOT / 'examples' / 'pypsa_multi_period.yaml', } #: One PyPSA reference network per rung, run out of band with the versions From f1ea95b710a9601b04a0d8151d26db7e43ef9ff0 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 22 Sep 2026 16:48:34 +0200 Subject: [PATCH 02/46] 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/examples/pypsa.md | 596 +++++++++++- examples/pypsa.yaml | 301 +++++- examples/references/pypsa/references.json | 61 ++ examples/references/pypsa/rung_17_process.py | 42 + .../references/pypsa/rung_18_transformer.py | 63 ++ examples/symbols/pypsa.yaml | 39 + tests/fixtures/pypsa_standard_shape.yaml | 873 ++++++++++-------- tests/test_pypsa_collapses.py | 2 + 8 files changed, 1594 insertions(+), 383 deletions(-) create mode 100644 examples/references/pypsa/rung_17_process.py create mode 100644 examples/references/pypsa/rung_18_transformer.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 9e481055..b386864f 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1343,6 +1343,178 @@ def build(): +### Rung 17 — process + +A process is a generalized converter. It moves an internal power from `bus0` to +the buses it feeds, and each port draws or delivers at its own `rate`. A +non-extendable process carries a fixed capacity. An extendable one chooses its +capacity between `p_nom_min` and `p_nom_max`. A ramp limit caps the change in +internal power between snapshots. A `p_set` fixes an internal power schedule. A +`p_nom_set` fixes an extendable process's built capacity. The machinery is the +generator's and the link's, read over a converter. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Process-p`, `Process-p_nom`](#variable-domains) | done | internal power and capacity, as a link | +| [`Process-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#process-fix-p-lower) | done | rungs 1 and 3, over a converter | +| [`Process-p-ramp_limit_*`](#process-p-ramp_limit_up) | done | rung 4, on a non-committable converter | +| [`Process-p_set`](#process-p_set) | done | a fixed internal power schedule | +| [`Process-p_nom_set`](#process-p_nom_set) | done | a fixed built capacity | +| [`Bus-nodal_balance`](#bus-nodal_balance) | done | each port enters at its `rate` | +| [objective](#objective) | done | marginal cost on internal power; capital on capacity | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `9730.0`, 70 rows. + +
+The network, as PyPSA code + +`rung_17_process.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 17: process — generalized converters, one fixed and ramping, one extendable, one on a set schedule, all feeding a hub.""" + +from __future__ import annotations + +from math import nan + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'hub') + n.add( + 'Process', + 'conv_fix', + bus0='north', + bus1='hub', + p_nom=50, + marginal_cost=2, + ramp_limit_up=0.3, + ramp_limit_down=0.3, + ) + n.add( + 'Process', + 'conv_ext', + bus0='south', + bus1='hub', + p_nom_extendable=True, + capital_cost=20, + p_nom_min=5, + p_nom_max=40, + marginal_cost=1, + p_nom_set=25, + ) + n.add('Process', 'conv_set', bus0='north', bus1='hub', p_nom=20, marginal_cost=3, p_set=[10, nan, nan, nan]) + n.add('Load', 'hub_load', bus='hub', p_set=[15, 20, 25, 10]) + return n +``` + +
+ + +### Rung 18 — transformer + +A transformer is a passive branch between two buses, as a line is, but its flow +follows its effective series reactance and a fixed phase shift. It obeys the +Kirchhoff voltage law (KVL) around every independent cycle, so it builds no flow +outside a mesh. A non-extendable transformer carries a fixed nominal apparent +power. An extendable one chooses it between `s_nom_min` and `s_nom_max`. An +`s_set` fixes a flow schedule. An `s_nom_set` fixes an extendable transformer's +built capacity. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Transformer-s`, `Transformer-s_nom`](#variable-domains) | done | flow and capacity, as a line | +| [`Transformer-fix-s-*`, `-ext-s-*`, `-ext-s_nom-*`](#transformer-fix-s-lower) | done | rungs 1 and 3, over a transformer | +| [`Transformer-s_set`](#transformer-s_set) | done | a fixed flow schedule | +| [`Transformer-s_nom_set`](#transformer-s_nom_set) | done | a fixed built capacity | +| [`Kirchhoff-Voltage-Law`](#kirchhoff-voltage-law) | done | rung 6, over `x_pu_eff` and a phase shift | +| [objective](#objective) | done | capital on capacity | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `12274.401472395122`, 106 rows. + +
+The network, as PyPSA code + +`rung_18_transformer.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 18: transformer — passive branches under KVL in a meshed triangle, one fixed and on a set flow, two extendable in parallel.""" + +from __future__ import annotations + +from math import nan + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'a') + n.add('Bus', 'b') + n.add('Bus', 'c') + n.add('Generator', 'hydro18', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel18', bus='b', p_nom=80, marginal_cost=50) + n.add('Load', 'town18', bus='c', p_set=45) + n.add('Line', 'ab', bus0='a', bus1='b', carrier='AC', length=30, x=0.1, r=0.01, s_nom=60) + n.add( + 'Transformer', + 'bc', + bus0='b', + bus1='c', + x=0.1, + r=0.01, + s_nom=60, + phase_shift=10, + s_set=[16, nan, nan, nan], + ) + n.add( + 'Transformer', + 'ca', + bus0='c', + bus1='a', + x=0.15, + r=0.01, + s_nom=60, + s_nom_extendable=True, + capital_cost=10, + s_nom_min=5, + s_nom_max=40, + s_nom_set=30, + tap_ratio=1.05, + ) + n.add( + 'Transformer', + 'ca2', + bus0='c', + bus1='a', + x=0.12, + r=0.01, + s_nom=60, + s_nom_extendable=True, + capital_cost=8, + s_nom_min=5, + s_nom_max=40, + ) + return n +``` + +
+ + ### Not on a rung | PyPSA | status | note | @@ -1379,14 +1551,17 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | | $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | | $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | | $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | | $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | +| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | +| $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | | $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | | $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | | $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a fixed phase shift | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | | $`\mathcal{B}`$ | index $`b`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | | $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | @@ -1428,6 +1603,21 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once | | $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the horizon's end — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at the first snapshots is lost | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | +| $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\mathcal{J}`$ — nominal internal power | +| $`\mathrm{ext}^{z}`$ | `Process_p_nom_extendable` over $`\mathcal{J}`$ — whether the nominal internal power is a decision | +| $`\underline{\mathrm{z}}`$ | `Process_p_min_pu` over $`\mathcal{T} \times \mathcal{J}`$ — least internal power, per unit of nominal power — negative for a process that runs both ways | +| $`\overline{\mathrm{z}}`$ | `Process_p_max_pu` over $`\mathcal{T} \times \mathcal{J}`$ — most internal power, per unit of nominal power | +| $`\alpha`$ | `Process_rate` over $`\mathcal{R}`$ — the energy a port draws or delivers per unit of internal power, PyPSA's `rate0`, `rate1`, … read long — negative where the port withdraws, positive where it injects; a link is a process whose `bus0` rate is minus one and whose output rates are its efficiencies | +| $`\mathrm{d}^{z}`$ | `Process_output_delay` over $`\mathcal{R}`$ — snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that transfers at once | +| $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the horizon's end — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at the first snapshots is lost | +| $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | +| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\mathcal{T} \times \mathcal{J}`$ — a given internal power schedule; a process without one has no row here | +| $`\underline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_min` over $`\mathcal{J}`$ — least nominal power an extendable process may be built at | +| $`\overline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_max` over $`\mathcal{J}`$ — most nominal power an extendable process may be built at | +| $`\mathrm{c}^{\mathrm{cap},z}`$ | `Process_capital_cost` over $`\mathcal{J}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\mathcal{J}`$ — a given nominal power for an extendable process; one without a value has no row here | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | @@ -1438,11 +1628,15 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{on}^{h}`$ | `StorageUnit_active` over $`\mathcal{T} \times \mathcal{S}`$ — whether a storage unit stands in a snapshot's period — PyPSA's `active`, data prep | | $`\mathrm{on}^{e}`$ | `Store_active` over $`\mathcal{T} \times \mathcal{V}`$ — whether a store stands in a snapshot's period — PyPSA's `active`, data prep | | $`\mathrm{on}^{s}`$ | `Line_active` over $`\mathcal{T} \times \mathcal{K}`$ — whether a line stands in a snapshot's period — PyPSA's `active`, data prep | +| $`\mathrm{on}^{z}`$ | `Process_active` over $`\mathcal{T} \times \mathcal{J}`$ — whether a process stands in a snapshot's period — PyPSA's `active`, data prep | +| $`\mathrm{on}^{\sigma}`$ | `Transformer_active` over $`\mathcal{T} \times \mathcal{M}`$ — whether a transformer stands in a snapshot's period — PyPSA's `active`, data prep | | $`\mathrm{W}`$ | `Generator_capital_weight` over $`\mathcal{G}`$ — the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{W}^{f}`$ | `Link_capital_weight` over $`\mathcal{L}`$ — the sum of period weights a link stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{W}^{h}`$ | `StorageUnit_capital_weight` over $`\mathcal{S}`$ — the sum of period weights a storage unit stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{W}^{e}`$ | `Store_capital_weight` over $`\mathcal{V}`$ — the sum of period weights a store stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{W}^{s}`$ | `Line_capital_weight` over $`\mathcal{K}`$ — the sum of period weights a line stands in — PyPSA's `active * period_weighting`, summed, data prep | +| $`\mathrm{W}^{z}`$ | `Process_capital_weight` over $`\mathcal{J}`$ — the sum of period weights a process stands in — PyPSA's `active * period_weighting`, summed, data prep | +| $`\mathrm{W}^{\sigma}`$ | `Transformer_capital_weight` over $`\mathcal{M}`$ — the sum of period weights a transformer stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | | $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit | | $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top | @@ -1503,6 +1697,16 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | | $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | | $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row | +| $`\sigma^{\mathrm{nom}}`$ | `Transformer_s_nom` over $`\mathcal{M}`$ — nominal apparent power | +| $`\mathrm{ext}^{\sigma}`$ | `Transformer_s_nom_extendable` over $`\mathcal{M}`$ — whether the nominal apparent power is a decision | +| $`\overline{\sigma}`$ | `Transformer_s_max_pu` over $`\mathcal{T} \times \mathcal{M}`$ — most flow either way, per unit of nominal apparent power | +| $`\underline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_min` over $`\mathcal{M}`$ — least nominal apparent power an extendable transformer may be built at | +| $`\overline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_max` over $`\mathcal{M}`$ — most nominal apparent power an extendable transformer may be built at | +| $`\mathrm{c}^{\mathrm{cap},\sigma}`$ | `Transformer_capital_cost` over $`\mathcal{M}`$ — cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$ — a given nominal apparent power for an extendable transformer; one without a value has no row here | +| $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | +| $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row | +| $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's fixed phase shift in radians, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; a transformer with no shift or in no cycle has no row. PyPSA also admits an optimisable phase shift, a later rung this file does not carry | | $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{B}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | | $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\mathcal{B}`$ — which way the row binds — `<=`, `>=` or `==` | | $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{B}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, a non-extendable build — is folded in here by data prep | @@ -1522,6 +1726,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it has no row | | $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{S}`$ — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it has no row | | $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{V}`$ — one where the store is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{J}`$ — one where the process is in the row's carrier-and-bus set — data prep; one outside it has no row | #### Variables @@ -1529,6 +1734,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file |---|---| | $`p`$ | `Generator_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-p` — output of a generator in a snapshot | | $`f`$ | `Link_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to | +| $`z`$ | `Process_p` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-p` — PyPSA's internal power `p`: a positive value drives every port at its own rate, withdrawing where the rate is negative and injecting where it is positive | | $`h^{+}`$ | `StorageUnit_p_dispatch` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_dispatch` — power delivered to the bus | | $`h^{-}`$ | `StorageUnit_p_store` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_store` — power drawn from the bus into charge | | $`\mathit{soc}`$ | `StorageUnit_state_of_charge` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-state_of_charge` — energy held at the end of a snapshot | @@ -1540,9 +1746,12 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{up}`$ | `Generator_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | | $`\mathit{dn}`$ | `Generator_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | +| $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`F`$ | `Link_p_nom_ext` over $`\mathcal{L}`$ — `Link-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | +| $`Z`$ | `Process_p_nom_ext` over $`\mathcal{J}`$ — `Process-p_nom` — nominal internal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | +| $`\Sigma`$ | `Transformer_s_nom_ext` over $`\mathcal{M}`$ — `Transformer-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`H`$ | `StorageUnit_p_nom_ext` over $`\mathcal{S}`$ — `StorageUnit-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`E`$ | `Store_e_nom_ext` over $`\mathcal{V}`$ — `Store-e_nom` — nominal capacity where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`a`$ | `CVaR_a` over $`\Xi`$ — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not | @@ -1559,9 +1768,11 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\Delta^{+}`$ | `Generator_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | +| $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | | $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | | $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | +| $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed transfer wraps from the horizon's end, and where it is not the energy still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | | $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{B}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | | $`\mathit{operational\_limit}`$ | `operational_limit` over $`\Xi \times \mathcal{B}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | | $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\mathcal{B}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | @@ -1587,12 +1798,14 @@ objective: + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + CVaR_omega * CVaR ``` ```math -\min \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR +\min \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{c}^{\mathrm{cap},z}_{j} \cdot \mathrm{W}^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \mathrm{c}^{\mathrm{cap},\sigma}_{m} \cdot \mathrm{W}^{\sigma}_{m} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR ``` ### `Generator-fix-p-lower` @@ -1851,6 +2064,118 @@ Link_p_nom_set: F_{l} = \mathrm{f}^{\mathrm{nom,set}}_{l} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{nom,set}}_{l} \text{ is defined} ``` +### `Process-fix-p-lower` + +`Process_fix_p_lower` + +```yaml +Process_fix_p_lower: + description: "`Process-fix-p-lower` — a fixed process runs at least its minimum, negative for the other way" + dims: [scenario, snapshot, process] + where: not Process_p_nom_extendable AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom +``` + +```math +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-fix-p-upper` + +`Process_fix_p_upper` + +```yaml +Process_fix_p_upper: + description: "`Process-fix-p-upper` — a fixed process runs at most its nominal power" + dims: [scenario, snapshot, process] + where: not Process_p_nom_extendable AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom +``` + +```math +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-ext-p-lower` + +`Process_ext_p_lower` + +```yaml +Process_ext_p_lower: + description: "`Process-ext-p-lower` — an extendable process runs at least its minimum of the chosen build, negative for the other way" + dims: [scenario, snapshot, process] + where: Process_p_nom_extendable AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom_ext +``` + +```math +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-ext-p-upper` + +`Process_ext_p_upper` + +```yaml +Process_ext_p_upper: + description: "`Process-ext-p-upper` — an extendable process runs at most the chosen build" + dims: [scenario, snapshot, process] + where: Process_p_nom_extendable AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom_ext +``` + +```math +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-ext-p_nom-lower` + +`Process_ext_p_nom_lower` + +```yaml +Process_ext_p_nom_lower: + description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor" + dims: [process] + where: Process_p_nom_extendable + expression: Process_p_nom_ext >= Process_p_nom_min +``` + +```math +Z_{j} \ge \underline{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} +``` + +### `Process-ext-p_nom-upper` + +`Process_ext_p_nom_upper` + +```yaml +Process_ext_p_nom_upper: + description: "`Process-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" + dims: [process] + where: Process_p_nom_extendable AND Process_p_nom_max + expression: Process_p_nom_ext <= Process_p_nom_max +``` + +```math +Z_{j} \le \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \text{ is defined} +``` + +### `Process-p_nom_set` + +`Process_p_nom_set` + +```yaml +Process_p_nom_set: + description: "`Process-p_nom_set` — the chosen build pinned, wherever a value is given" + dims: [process] + where: Process_p_nom_extendable AND Process_p_nom_set + expression: Process_p_nom_ext == Process_p_nom_set +``` + +```math +Z_{j} = \mathrm{z}^{\mathrm{nom,set}}_{j} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{nom,set}}_{j} \text{ is defined} +``` + ### `StorageUnit-fix-p_dispatch-lower` `StorageUnit_fix_p_dispatch_lower` @@ -2521,6 +2846,134 @@ Line_s_set: s_{\xi,t,k} = \mathrm{s}^{\mathrm{set}}_{t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{s}^{\mathrm{set}}_{t,k} \text{ is defined} \wedge \mathrm{on}^{s}_{t,k} ``` +### `Transformer-fix-s-lower` + +`Transformer_fix_s_lower` + +```yaml +Transformer_fix_s_lower: + description: "`Transformer-fix-s-lower` — a fixed transformer carries at least the negative of its rating" + dims: [scenario, snapshot, transformer] + where: not Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom +``` + +```math +\sigma_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + +### `Transformer-fix-s-upper` + +`Transformer_fix_s_upper` + +```yaml +Transformer_fix_s_upper: + description: "`Transformer-fix-s-upper` — a fixed transformer carries at most its rating" + dims: [scenario, snapshot, transformer] + where: not Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom +``` + +```math +\sigma_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + +### `Transformer-ext-s-lower` + +`Transformer_ext_s_lower` + +```yaml +Transformer_ext_s_lower: + description: "`Transformer-ext-s-lower` — an extendable transformer carries at least the negative of its rating of the chosen build" + dims: [scenario, snapshot, transformer] + where: Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom_ext +``` + +```math +\sigma_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + +### `Transformer-ext-s-upper` + +`Transformer_ext_s_upper` + +```yaml +Transformer_ext_s_upper: + description: "`Transformer-ext-s-upper` — an extendable transformer carries at most its rating of the chosen build" + dims: [scenario, snapshot, transformer] + where: Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom_ext +``` + +```math +\sigma_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + +### `Transformer-ext-s_nom-lower` + +`Transformer_ext_s_nom_lower` + +```yaml +Transformer_ext_s_nom_lower: + description: "`Transformer-ext-s_nom-lower` — the chosen build is at least its floor" + dims: [transformer] + where: Transformer_s_nom_extendable + expression: Transformer_s_nom_ext >= Transformer_s_nom_min +``` + +```math +\Sigma_{m} \ge \underline{\sigma}^{\mathrm{nom}}_{m} \qquad \forall\, m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} +``` + +### `Transformer-ext-s_nom-upper` + +`Transformer_ext_s_nom_upper` + +```yaml +Transformer_ext_s_nom_upper: + description: "`Transformer-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" + dims: [transformer] + where: Transformer_s_nom_extendable AND Transformer_s_nom_max + expression: Transformer_s_nom_ext <= Transformer_s_nom_max +``` + +```math +\Sigma_{m} \le \overline{\sigma}^{\mathrm{nom}}_{m} \qquad \forall\, m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \overline{\sigma}^{\mathrm{nom}}_{m} \text{ is defined} +``` + +### `Transformer-s_nom_set` + +`Transformer_s_nom_set` + +```yaml +Transformer_s_nom_set: + description: "`Transformer-s_nom_set` — the chosen build pinned, wherever a value is given" + dims: [transformer] + where: Transformer_s_nom_extendable AND Transformer_s_nom_set + expression: Transformer_s_nom_ext == Transformer_s_nom_set +``` + +```math +\Sigma_{m} = \sigma^{\mathrm{nom,set}}_{m} \qquad \forall\, m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \sigma^{\mathrm{nom,set}}_{m} \text{ is defined} +``` + +### `Transformer-s_set` + +`Transformer_s_set` + +```yaml +Transformer_s_set: + description: "`Transformer-s_set` — flow pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, transformer] + where: Transformer_s_set AND Transformer_active + expression: Transformer_s == Transformer_s_set +``` + +```math +\sigma_{\xi,t,m} = \sigma^{\mathrm{set}}_{t,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \sigma^{\mathrm{set}}_{t,m} \text{ is defined} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + ### `Kirchhoff-Voltage-Law` `Kirchhoff_Voltage_Law` @@ -2530,13 +2983,18 @@ Kirchhoff_Voltage_Law: description: >- `Kirchhoff-Voltage-Law` — around every independent cycle the impedance-weighted flows sum to nothing, which is what makes the linear - power flow physical rather than transport + power flow physical rather than transport. A transformer's flow weighs its + effective reactance, and its fixed phase shift enters the cycle sum as a + constant dims: [scenario, snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 + expression: >- + sum(Line_s * Line_cycle_weight, over=line) + + sum(Transformer_s * Transformer_cycle_weight, over=transformer) + + sum(Transformer_phase_shift_weight, over=transformer) == 0 ``` ```math -\sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{k,c} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ c \in \mathcal{C} +\sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{k,c} + \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{m,c} + \sum_{m \in \mathcal{M}} \vartheta_{m,c} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ c \in \mathcal{C} ``` ### `Generator-p-ramp_limit_up` @@ -2622,6 +3080,41 @@ Link_p_ramp_limit_down: f_{\xi,t - 1,l} - f_{\xi,t,l} \le \mathrm{rd}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} ``` +### `Process-p-ramp_limit_up` + +`Process_p_ramp_limit_up` + +```yaml +Process_p_ramp_limit_up: + description: >- + `Process-p-ramp_limit_up` — a process raises internal power no faster than + its limit of the build. The translated term vacates the first snapshot, + where a plain optimize builds no row either + dims: [scenario, snapshot, process] + where: Process_ramp_limit_up AND Process_active + expression: Process_p - shift(Process_p, along=snapshot, offset=1) <= Process_ramp_limit_up * Process_p_nom_effective +``` + +```math +z_{\xi,t,j} - z_{\xi,t - 1,j} \le \mathrm{ru}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-p-ramp_limit_down` + +`Process_p_ramp_limit_down` + +```yaml +Process_p_ramp_limit_down: + description: "`Process-p-ramp_limit_down` — a process lowers internal power no faster than its limit of the build" + dims: [scenario, snapshot, process] + where: Process_ramp_limit_down AND Process_active + expression: shift(Process_p, along=snapshot, offset=1) - Process_p <= Process_ramp_limit_down * Process_p_nom_effective +``` + +```math +z_{\xi,t - 1,j} - z_{\xi,t,j} \le \mathrm{rd}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \mathrm{on}^{z}_{t,j} +``` + ### `StorageUnit-ext-p_dispatch-lower` `StorageUnit_ext_p_dispatch_lower` @@ -2955,6 +3448,22 @@ Link_p_set: f_{\xi,t,l} = \mathrm{f}^{\mathrm{set}}_{t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{f}^{\mathrm{set}}_{t,l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} ``` +### `Process-p_set` + +`Process_p_set` + +```yaml +Process_p_set: + description: "`Process-p_set` — internal power pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, process] + where: Process_p_set AND Process_active + expression: Process_p == Process_p_set +``` + +```math +z_{\xi,t,j} = \mathrm{z}^{\mathrm{set}}_{t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{z}^{\mathrm{set}}_{t,j} \text{ is defined} \wedge \mathrm{on}^{z}_{t,j} +``` + ### `StorageUnit-p_set` `StorageUnit_p_set` @@ -3252,8 +3761,9 @@ Bus_nodal_balance: description: >- `Bus-nodal_balance` — what is generated at a bus, storage dispatch and stores included, less what the links take away, plus what arrives over - them after losses and any delay at every port they deliver to, meets the - load there. + them after losses and any delay at every port they deliver to, each + process port drawing or delivering at its own rate and each passive branch + carrying its flow, meets the load there. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] @@ -3263,13 +3773,16 @@ Bus_nodal_balance: + sum(Store_p, by=Store_bus, over=store, into=bus) - sum(Link_p, by=Link_bus0, over=link, into=bus) + sum(Link_output_arrival, by=Link_output_bus, over=link_output, into=bus) + + sum(Process_output_arrival, by=Process_output_bus, over=process_output, into=bus) - sum(Line_s, by=Line_bus0, over=line, into=bus) + sum(Line_s, by=Line_bus1, over=line, into=bus) + - sum(Transformer_s, by=Transformer_bus0, over=transformer, into=bus) + + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) == sum(Load_p_set, by=Load_bus, over=load, into=bus) ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} ``` ### `Carrier-growth_limit` @@ -3436,6 +3949,21 @@ Link_p_nom_effective: \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} = \begin{cases} F_{l} & \text{if } \mathrm{ext}^{f}_{l} \\ \mathrm{f}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} ``` +### `Process_p_nom_effective` + +```yaml +Process_p_nom_effective: + description: the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise + dims: [process] + cases: + extendable: { when: Process_p_nom_extendable, expression: Process_p_nom_ext } + otherwise: Process_p_nom +``` + +```math +\widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} = \begin{cases} Z_{j} & \text{if } \mathrm{ext}^{z}_{j} \\ \mathrm{z}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +``` + ### `StorageUnit_charge_carried_in` ```yaml @@ -3507,6 +4035,28 @@ Link_output_arrival: \overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} ``` +### `Process_output_arrival` + +```yaml +Process_output_arrival: + description: >- + what a process transfers at a port at a snapshot — its internal power + times the port's rate, delayed by the port's `delay`; where the port is + `cyclic_delay` the delayed transfer wraps from the horizon's end, and where + it is not the energy still in transit at the first snapshots is lost. A + port that does not delay (`delay` zero) transfers at once, cyclic or not + dims: [scenario, snapshot, process_output] + cases: + wrapping: + when: Process_output_cyclic_delay + expression: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge='wrap') + otherwise: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge=0) +``` + +```math +\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} +``` + ### `primary_energy` ```yaml @@ -3582,10 +4132,11 @@ tech_capacity_expansion: + sum(Line_s_nom_ext * Line_tech_capacity_weight, over=line) + sum(StorageUnit_p_nom_ext * StorageUnit_tech_capacity_weight, over=storage_unit) + sum(Store_e_nom_ext * Store_tech_capacity_weight, over=store) + + sum(Process_p_nom_ext * Process_tech_capacity_weight, over=process) ``` ```math -\mathit{tech\_capacity\_expansion}_{b} = \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{m}_{b,g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{m}^{f}_{b,l} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{m}^{l}_{b,k} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{m}^{h}_{b,s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{m}^{e}_{b,v} \qquad \forall\, b \in \mathcal{B} +\mathit{tech\_capacity\_expansion}_{b} = \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{m}_{b,g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{m}^{f}_{b,l} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{m}^{l}_{b,k} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{m}^{h}_{b,s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{m}^{e}_{b,v} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{m}^{z}_{b,j} \qquad \forall\, b \in \mathcal{B} ``` ### `scenario_opex` @@ -3596,6 +4147,7 @@ scenario_opex: expression: >- sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Process_p * Process_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) @@ -3607,7 +4159,7 @@ scenario_opex: ``` ```math -\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi ``` #### Variable domains @@ -3624,6 +4176,12 @@ p_{\xi,t,g} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g f_{\xi,t,l} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{on}^{f}_{t,l} ``` +**`Process_p`** + +```math +z_{\xi,t,j} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{on}^{z}_{t,j} +``` + **`StorageUnit_p_dispatch`** ```math @@ -3690,6 +4248,12 @@ u_{\xi,t,g} \ge 0, u_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \ s_{\xi,t,k} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{on}^{s}_{t,k} ``` +**`Transformer_s`** + +```math +\sigma_{\xi,t,m} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{on}^{\sigma}_{t,m} +``` + **`Line_s_nom_ext`** ```math @@ -3708,6 +4272,18 @@ P_{g} \in \mathbb{R} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} F_{l} \in \mathbb{R} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} ``` +**`Process_p_nom_ext`** + +```math +Z_{j} \in \mathbb{R} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} +``` + +**`Transformer_s_nom_ext`** + +```math +\Sigma_{m} \in \mathbb{R} \qquad \forall\, m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} +``` + **`StorageUnit_p_nom_ext`** ```math diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index f4640fb7..4e6f7a2c 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -30,6 +30,13 @@ dimensions: a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep + process: + description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate + process_output: + description: >- + a process's ports, one label per port a process declares — PyPSA's + `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number + of ports is one term in the balance, data prep load: description: demands, each on one bus storage_unit: @@ -38,6 +45,8 @@ dimensions: description: pure energy stores, each on one bus line: description: passive branches, each between two buses, their flow set by impedance + transformer: + description: passive branches between two buses, their flow set by impedance and tap ratio, with a fixed phase shift cycle: description: independent cycles of the passive network graph — the cycle basis, data prep global_constraint: @@ -173,6 +182,65 @@ parameters: Link_marginal_cost: description: cost of one unit of flow dims: [snapshot, link] + Process_p_nom: + description: nominal internal power + dims: [process] + Process_p_nom_extendable: + description: whether the nominal internal power is a decision + dims: [process] + dtype: bool + Process_p_min_pu: + description: least internal power, per unit of nominal power — negative for a process that runs both ways + dims: [snapshot, process] + Process_p_max_pu: + description: most internal power, per unit of nominal power + dims: [snapshot, process] + Process_rate: + description: >- + the energy a port draws or delivers per unit of internal power, PyPSA's + `rate0`, `rate1`, … read long — negative where the port withdraws, + positive where it injects; a link is a process whose `bus0` rate is minus + one and whose output rates are its efficiencies + dims: [process_output] + Process_output_delay: + description: >- + snapshots a port's transfer lags its process's internal power — PyPSA's + `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` + units, which the file states as whole snapshots; zero for a port that + transfers at once + dims: [process_output] + dtype: int + Process_output_cyclic_delay: + description: >- + whether a delayed port's transfer wraps from the horizon's end — PyPSA's + `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still + in transit at the first snapshots is lost + dims: [process_output] + dtype: bool + Process_marginal_cost: + description: cost of one unit of internal power + dims: [snapshot, process] + Process_ramp_limit_up: + description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit + dims: [process] + Process_ramp_limit_down: + description: most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit + dims: [process] + Process_p_set: + description: a given internal power schedule; a process without one has no row here + dims: [snapshot, process] + Process_p_nom_min: + description: least nominal power an extendable process may be built at + dims: [process] + Process_p_nom_max: + description: most nominal power an extendable process may be built at + dims: [process] + Process_capital_cost: + description: cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep + dims: [process] + Process_p_nom_set: + description: a given nominal power for an extendable process; one without a value has no row here + dims: [process] Load_p_set: description: demand dims: [scenario, snapshot, load] @@ -208,6 +276,14 @@ parameters: description: whether a line stands in a snapshot's period — PyPSA's `active`, data prep dims: [snapshot, line] dtype: bool + Process_active: + description: whether a process stands in a snapshot's period — PyPSA's `active`, data prep + dims: [snapshot, process] + dtype: bool + Transformer_active: + description: whether a transformer stands in a snapshot's period — PyPSA's `active`, data prep + dims: [snapshot, transformer] + dtype: bool Generator_capital_weight: description: the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep dims: [generator] @@ -223,6 +299,12 @@ parameters: Line_capital_weight: description: the sum of period weights a line stands in — PyPSA's `active * period_weighting`, summed, data prep dims: [line] + Process_capital_weight: + description: the sum of period weights a process stands in — PyPSA's `active * period_weighting`, summed, data prep + dims: [process] + Transformer_capital_weight: + description: the sum of period weights a transformer stands in — PyPSA's `active * period_weighting`, summed, data prep + dims: [transformer] Generator_first_active: description: one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep dims: [period, generator] @@ -410,6 +492,44 @@ parameters: the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row dims: [line, cycle] + Transformer_s_nom: + description: nominal apparent power + dims: [transformer] + Transformer_s_nom_extendable: + description: whether the nominal apparent power is a decision + dims: [transformer] + dtype: bool + Transformer_s_max_pu: + description: most flow either way, per unit of nominal apparent power + dims: [snapshot, transformer] + Transformer_s_nom_min: + description: least nominal apparent power an extendable transformer may be built at + dims: [transformer] + Transformer_s_nom_max: + description: most nominal apparent power an extendable transformer may be built at + dims: [transformer] + Transformer_capital_cost: + description: cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep + dims: [transformer] + Transformer_s_nom_set: + description: a given nominal apparent power for an extendable transformer; one without a value has no row here + dims: [transformer] + Transformer_s_set: + description: a given flow schedule; a transformer without one has no row here + dims: [snapshot, transformer] + Transformer_cycle_weight: + description: >- + the transformer's effective series reactance, `x` times its tap ratio, + signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle + basis, data prep; a transformer in no cycle has no row + dims: [transformer, cycle] + Transformer_phase_shift_weight: + description: >- + the transformer's fixed phase shift in radians, signed by its orientation + in the cycle — a constant added to the cycle sum, data prep; a transformer + with no shift or in no cycle has no row. PyPSA also admits an optimisable + phase shift, a later rung this file does not carry + dims: [transformer, cycle] GlobalConstraint_type: description: >- which formula the row takes — `primary_energy`, `operational_limit`, @@ -482,6 +602,9 @@ parameters: Store_tech_capacity_weight: description: one where the store is in the row's carrier-and-bus set — data prep; one outside it has no row dims: [global_constraint, store] + Process_tech_capacity_weight: + description: one where the process is in the row's carrier-and-bus set — data prep; one outside it has no row + dims: [global_constraint, process] relations: snapshot_period: @@ -511,6 +634,17 @@ relations: relation, so the file states any number of them key: link_output values: bus + Process_output_process: + description: the process a port belongs to + key: process_output + values: process + Process_output_bus: + description: >- + the bus a port draws from or delivers to — PyPSA's `bus0`, `bus1`, … + columns. A process of three ports is three labels here rather than a + third relation, so the file states any number of them + key: process_output + values: bus Load_bus: description: the bus a load sits on key: load @@ -531,6 +665,14 @@ relations: description: the bus a store sits on key: store values: bus + Transformer_bus0: + description: the bus a transformer's flow is measured at + key: transformer + values: bus + Transformer_bus1: + description: the bus at a transformer's other end + key: transformer + values: bus variables: Generator_p: @@ -544,6 +686,13 @@ variables: output ports deliver to dims: [scenario, snapshot, link] where: Link_active + Process_p: + description: >- + `Process-p` — PyPSA's internal power `p`: a positive value drives every + port at its own rate, withdrawing where the rate is negative and injecting + where it is positive + dims: [scenario, snapshot, process] + where: Process_active StorageUnit_p_dispatch: description: "`StorageUnit-p_dispatch` — power delivered to the bus" dims: [scenario, snapshot, storage_unit] @@ -612,6 +761,13 @@ variables: positive value withdraws there and injects at `Line_bus1`, lossless dims: [scenario, snapshot, line] where: Line_active + Transformer_s: + description: >- + `Transformer-s` — PyPSA's `p0`, the flow measured at the + `Transformer_bus0` end: a positive value withdraws there and injects at + `Transformer_bus1`, lossless + dims: [scenario, snapshot, transformer] + where: Transformer_active Line_s_nom_ext: description: >- `Line-s_nom` — nominal apparent power where it is a decision; the @@ -630,6 +786,18 @@ variables: the same PyPSA name carries the fixed regime dims: [link] where: Link_p_nom_extendable + Process_p_nom_ext: + description: >- + `Process-p_nom` — nominal internal power where it is a decision; the + parameter of the same PyPSA name carries the fixed regime + dims: [process] + where: Process_p_nom_extendable + Transformer_s_nom_ext: + description: >- + `Transformer-s_nom` — nominal apparent power where it is a decision; the + parameter of the same PyPSA name carries the fixed regime + dims: [transformer] + where: Transformer_s_nom_extendable StorageUnit_p_nom_ext: description: >- `StorageUnit-p_nom` — nominal power where it is a decision; the @@ -712,6 +880,12 @@ expressions: cases: extendable: { when: Link_p_nom_extendable, expression: Link_p_nom_ext } otherwise: Link_p_nom + Process_p_nom_effective: + description: the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise + dims: [process] + cases: + extendable: { when: Process_p_nom_extendable, expression: Process_p_nom_ext } + otherwise: Process_p_nom StorageUnit_charge_carried_in: description: >- the charge a unit opens a snapshot with — its last snapshot's less @@ -756,6 +930,19 @@ expressions: when: Link_output_cyclic_delay expression: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge='wrap') otherwise: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge=0) + Process_output_arrival: + description: >- + what a process transfers at a port at a snapshot — its internal power + times the port's rate, delayed by the port's `delay`; where the port is + `cyclic_delay` the delayed transfer wraps from the horizon's end, and where + it is not the energy still in transit at the first snapshots is lost. A + port that does not delay (`delay` zero) transfers at once, cyclic or not + dims: [scenario, snapshot, process_output] + cases: + wrapping: + when: Process_output_cyclic_delay + expression: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge='wrap') + otherwise: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge=0) primary_energy: description: >- what a `primary_energy` row totals — weighted generator energy, less @@ -792,11 +979,13 @@ expressions: + sum(Line_s_nom_ext * Line_tech_capacity_weight, over=line) + sum(StorageUnit_p_nom_ext * StorageUnit_tech_capacity_weight, over=storage_unit) + sum(Store_e_nom_ext * Store_tech_capacity_weight, over=store) + + sum(Process_p_nom_ext * Process_tech_capacity_weight, over=process) scenario_opex: description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight expression: >- sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Process_p * Process_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) @@ -887,6 +1076,41 @@ constraints: dims: [link] where: Link_p_nom_extendable AND Link_p_nom_set expression: Link_p_nom_ext == Link_p_nom_set + Process_fix_p_lower: + description: "`Process-fix-p-lower` — a fixed process runs at least its minimum, negative for the other way" + dims: [scenario, snapshot, process] + where: not Process_p_nom_extendable AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom + Process_fix_p_upper: + description: "`Process-fix-p-upper` — a fixed process runs at most its nominal power" + dims: [scenario, snapshot, process] + where: not Process_p_nom_extendable AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom + Process_ext_p_lower: + description: "`Process-ext-p-lower` — an extendable process runs at least its minimum of the chosen build, negative for the other way" + dims: [scenario, snapshot, process] + where: Process_p_nom_extendable AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom_ext + Process_ext_p_upper: + description: "`Process-ext-p-upper` — an extendable process runs at most the chosen build" + dims: [scenario, snapshot, process] + where: Process_p_nom_extendable AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom_ext + Process_ext_p_nom_lower: + description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor" + dims: [process] + where: Process_p_nom_extendable + expression: Process_p_nom_ext >= Process_p_nom_min + Process_ext_p_nom_upper: + description: "`Process-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" + dims: [process] + where: Process_p_nom_extendable AND Process_p_nom_max + expression: Process_p_nom_ext <= Process_p_nom_max + Process_p_nom_set: + description: "`Process-p_nom_set` — the chosen build pinned, wherever a value is given" + dims: [process] + where: Process_p_nom_extendable AND Process_p_nom_set + expression: Process_p_nom_ext == Process_p_nom_set StorageUnit_fix_p_dispatch_lower: description: "`StorageUnit-fix-p_dispatch-lower` — dispatch is non-negative" dims: [scenario, snapshot, storage_unit] @@ -1139,13 +1363,58 @@ constraints: dims: [scenario, snapshot, line] where: Line_s_set AND Line_active expression: Line_s == Line_s_set + Transformer_fix_s_lower: + description: "`Transformer-fix-s-lower` — a fixed transformer carries at least the negative of its rating" + dims: [scenario, snapshot, transformer] + where: not Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom + Transformer_fix_s_upper: + description: "`Transformer-fix-s-upper` — a fixed transformer carries at most its rating" + dims: [scenario, snapshot, transformer] + where: not Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom + Transformer_ext_s_lower: + description: "`Transformer-ext-s-lower` — an extendable transformer carries at least the negative of its rating of the chosen build" + dims: [scenario, snapshot, transformer] + where: Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + Transformer_ext_s_upper: + description: "`Transformer-ext-s-upper` — an extendable transformer carries at most its rating of the chosen build" + dims: [scenario, snapshot, transformer] + where: Transformer_s_nom_extendable AND Transformer_active + expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom_ext + Transformer_ext_s_nom_lower: + description: "`Transformer-ext-s_nom-lower` — the chosen build is at least its floor" + dims: [transformer] + where: Transformer_s_nom_extendable + expression: Transformer_s_nom_ext >= Transformer_s_nom_min + Transformer_ext_s_nom_upper: + description: "`Transformer-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" + dims: [transformer] + where: Transformer_s_nom_extendable AND Transformer_s_nom_max + expression: Transformer_s_nom_ext <= Transformer_s_nom_max + Transformer_s_nom_set: + description: "`Transformer-s_nom_set` — the chosen build pinned, wherever a value is given" + dims: [transformer] + where: Transformer_s_nom_extendable AND Transformer_s_nom_set + expression: Transformer_s_nom_ext == Transformer_s_nom_set + Transformer_s_set: + description: "`Transformer-s_set` — flow pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, transformer] + where: Transformer_s_set AND Transformer_active + expression: Transformer_s == Transformer_s_set Kirchhoff_Voltage_Law: description: >- `Kirchhoff-Voltage-Law` — around every independent cycle the impedance-weighted flows sum to nothing, which is what makes the linear - power flow physical rather than transport + power flow physical rather than transport. A transformer's flow weighs its + effective reactance, and its fixed phase shift enters the cycle sum as a + constant dims: [scenario, snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 + expression: >- + sum(Line_s * Line_cycle_weight, over=line) + + sum(Transformer_s * Transformer_cycle_weight, over=transformer) + + sum(Transformer_phase_shift_weight, over=transformer) == 0 Generator_p_ramp_limit_up: description: >- `Generator-p-ramp_limit_up` — a generator raises output no faster than @@ -1185,6 +1454,19 @@ constraints: dims: [scenario, snapshot, link] where: Link_ramp_limit_down AND Link_active expression: shift(Link_p, along=snapshot, offset=1) - Link_p <= Link_ramp_limit_down * Link_p_nom_effective + Process_p_ramp_limit_up: + description: >- + `Process-p-ramp_limit_up` — a process raises internal power no faster than + its limit of the build. The translated term vacates the first snapshot, + where a plain optimize builds no row either + dims: [scenario, snapshot, process] + where: Process_ramp_limit_up AND Process_active + expression: Process_p - shift(Process_p, along=snapshot, offset=1) <= Process_ramp_limit_up * Process_p_nom_effective + Process_p_ramp_limit_down: + description: "`Process-p-ramp_limit_down` — a process lowers internal power no faster than its limit of the build" + dims: [scenario, snapshot, process] + where: Process_ramp_limit_down AND Process_active + expression: shift(Process_p, along=snapshot, offset=1) - Process_p <= Process_ramp_limit_down * Process_p_nom_effective StorageUnit_ext_p_dispatch_lower: description: "`StorageUnit-ext-p_dispatch-lower` — dispatch is non-negative" dims: [scenario, snapshot, storage_unit] @@ -1298,6 +1580,11 @@ constraints: dims: [scenario, snapshot, link] where: Link_p_set AND Link_active expression: Link_p == Link_p_set + Process_p_set: + description: "`Process-p_set` — internal power pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, process] + where: Process_p_set AND Process_active + expression: Process_p == Process_p_set StorageUnit_p_set: description: "`StorageUnit-p_set` — net dispatch pinned to the given schedule, wherever one is given" dims: [scenario, snapshot, storage_unit] @@ -1392,8 +1679,9 @@ constraints: description: >- `Bus-nodal_balance` — what is generated at a bus, storage dispatch and stores included, less what the links take away, plus what arrives over - them after losses and any delay at every port they deliver to, meets the - load there. + them after losses and any delay at every port they deliver to, each + process port drawing or delivering at its own rate and each passive branch + carrying its flow, meets the load there. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] @@ -1403,8 +1691,11 @@ constraints: + sum(Store_p, by=Store_bus, over=store, into=bus) - sum(Link_p, by=Link_bus0, over=link, into=bus) + sum(Link_output_arrival, by=Link_output_bus, over=link_output, into=bus) + + sum(Process_output_arrival, by=Process_output_bus, over=process_output, into=bus) - sum(Line_s, by=Line_bus0, over=line, into=bus) + sum(Line_s, by=Line_bus1, over=line, into=bus) + - sum(Transformer_s, by=Transformer_bus0, over=transformer, into=bus) + + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) == sum(Load_p_set, by=Load_bus, over=load, into=bus) Carrier_growth_limit: description: >- @@ -1436,5 +1727,7 @@ objective: + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + CVaR_omega * CVaR diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index dc60e459..ac9fd5dd 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -582,5 +582,66 @@ "Link-fix-p-lower": 8, "Link-fix-p-upper": 8 } + }, + "rung_17_process": { + "columns": { + "Generator-p": 8, + "Link-p": 4, + "Process-p": 12, + "Process-p_nom": 1 + }, + "global_constraints": {}, + "objective": 9730.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Process-ext-p-lower": 4, + "Process-ext-p-upper": 4, + "Process-ext-p_nom-lower": 1, + "Process-ext-p_nom-upper": 1, + "Process-fix-p-lower": 8, + "Process-fix-p-upper": 8, + "Process-p-ramp_limit_down": 3, + "Process-p-ramp_limit_up": 3, + "Process-p_nom_set": 1, + "Process-p_set": 1 + } + }, + "rung_18_transformer": { + "columns": { + "Generator-p": 16, + "Line-s": 4, + "Link-p": 4, + "Transformer-s": 12, + "Transformer-s_nom": 2, + "objective_constant": 1 + }, + "global_constraints": {}, + "objective": 12274.401472395122, + "objective_constant": 1080.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 20, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Kirchhoff-Voltage-Law": 8, + "Line-fix-s-lower": 4, + "Line-fix-s-upper": 4, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Transformer-ext-s-lower": 8, + "Transformer-ext-s-upper": 8, + "Transformer-ext-s_nom-lower": 2, + "Transformer-ext-s_nom-upper": 2, + "Transformer-fix-s-lower": 4, + "Transformer-fix-s-upper": 4, + "Transformer-s_nom_set": 1, + "Transformer-s_set": 1 + } } } diff --git a/examples/references/pypsa/rung_17_process.py b/examples/references/pypsa/rung_17_process.py new file mode 100644 index 00000000..273df6ea --- /dev/null +++ b/examples/references/pypsa/rung_17_process.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 17: process — generalized converters, one fixed and ramping, one extendable, one on a set schedule, all feeding a hub.""" + +from __future__ import annotations + +from math import nan + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'hub') + n.add( + 'Process', + 'conv_fix', + bus0='north', + bus1='hub', + p_nom=50, + marginal_cost=2, + ramp_limit_up=0.3, + ramp_limit_down=0.3, + ) + n.add( + 'Process', + 'conv_ext', + bus0='south', + bus1='hub', + p_nom_extendable=True, + capital_cost=20, + p_nom_min=5, + p_nom_max=40, + marginal_cost=1, + p_nom_set=25, + ) + n.add('Process', 'conv_set', bus0='north', bus1='hub', p_nom=20, marginal_cost=3, p_set=[10, nan, nan, nan]) + n.add('Load', 'hub_load', bus='hub', p_set=[15, 20, 25, 10]) + return n diff --git a/examples/references/pypsa/rung_18_transformer.py b/examples/references/pypsa/rung_18_transformer.py new file mode 100644 index 00000000..9ad1f59f --- /dev/null +++ b/examples/references/pypsa/rung_18_transformer.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 18: transformer — passive branches under KVL in a meshed triangle, one fixed and on a set flow, two extendable in parallel.""" + +from __future__ import annotations + +from math import nan + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'a') + n.add('Bus', 'b') + n.add('Bus', 'c') + n.add('Generator', 'hydro18', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel18', bus='b', p_nom=80, marginal_cost=50) + n.add('Load', 'town18', bus='c', p_set=45) + n.add('Line', 'ab', bus0='a', bus1='b', carrier='AC', length=30, x=0.1, r=0.01, s_nom=60) + n.add( + 'Transformer', + 'bc', + bus0='b', + bus1='c', + x=0.1, + r=0.01, + s_nom=60, + phase_shift=10, + s_set=[16, nan, nan, nan], + ) + n.add( + 'Transformer', + 'ca', + bus0='c', + bus1='a', + x=0.15, + r=0.01, + s_nom=60, + s_nom_extendable=True, + capital_cost=10, + s_nom_min=5, + s_nom_max=40, + s_nom_set=30, + tap_ratio=1.05, + ) + n.add( + 'Transformer', + 'ca2', + bus0='c', + bus1='a', + x=0.12, + r=0.01, + s_nom=60, + s_nom_extendable=True, + capital_cost=8, + s_nom_min=5, + s_nom_max=40, + ) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 9c871565..07f01892 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -15,6 +15,9 @@ dimensions: storage_unit: { index: s, set: '\mathcal{S}' } store: { index: v, set: '\mathcal{V}' } line: { index: k, set: '\mathcal{K}' } + transformer: { index: m, set: '\mathcal{M}' } + process: { index: j, set: '\mathcal{J}' } + process_output: { index: r, set: '\mathcal{R}' } cycle: { index: c, set: '\mathcal{C}' } scenario: { index: '\xi', set: '\Xi' } period: { index: y, set: '\mathcal{Y}' } @@ -82,6 +85,25 @@ names: Link_p_nom_max: '\overline{\mathrm{f}}^{\mathrm{nom}}' Link_capital_cost: '\mathrm{c}^{\mathrm{cap},f}' Link_p_nom_set: '\mathrm{f}^{\mathrm{nom,set}}' + Process_p: z + Process_p_nom: '\mathrm{z}^{\mathrm{nom}}' + Process_p_nom_extendable: '\mathrm{ext}^{z}' + Process_p_min_pu: '\underline{\mathrm{z}}' + Process_p_max_pu: '\overline{\mathrm{z}}' + Process_rate: '\alpha' + Process_output_delay: '\mathrm{d}^{z}' + Process_output_cyclic_delay: '\mathrm{cyc}^{z}' + Process_marginal_cost: '\mathrm{c}^{z}' + Process_ramp_limit_up: '\mathrm{ru}^{z}' + Process_ramp_limit_down: '\mathrm{rd}^{z}' + Process_p_set: '\mathrm{z}^{\mathrm{set}}' + Process_p_nom_min: '\underline{\mathrm{z}}^{\mathrm{nom}}' + Process_p_nom_max: '\overline{\mathrm{z}}^{\mathrm{nom}}' + Process_capital_cost: '\mathrm{c}^{\mathrm{cap},z}' + Process_p_nom_set: '\mathrm{z}^{\mathrm{nom,set}}' + Process_p_nom_ext: "Z" + Process_p_nom_effective: '\widetilde{\mathrm{z}}^{\mathrm{nom}}' + Process_output_arrival: '\overrightarrow{z}' StorageUnit_p_nom_ext: "H" StorageUnit_p_nom_min: '\underline{\mathrm{h}}^{\mathrm{nom}}' StorageUnit_p_nom_max: '\overline{\mathrm{h}}^{\mathrm{nom}}' @@ -103,6 +125,18 @@ names: Line_s_nom_set: '\mathrm{s}^{\mathrm{nom,set}}' Line_s_set: '\mathrm{s}^{\mathrm{set}}' Line_cycle_weight: '\mathrm{x}' + Transformer_s: '\sigma' + Transformer_s_nom_ext: '\Sigma' + Transformer_s_nom: '\sigma^{\mathrm{nom}}' + Transformer_s_nom_extendable: '\mathrm{ext}^{\sigma}' + Transformer_s_max_pu: '\overline{\sigma}' + Transformer_s_nom_min: '\underline{\sigma}^{\mathrm{nom}}' + Transformer_s_nom_max: '\overline{\sigma}^{\mathrm{nom}}' + Transformer_capital_cost: '\mathrm{c}^{\mathrm{cap},\sigma}' + Transformer_s_nom_set: '\sigma^{\mathrm{nom,set}}' + Transformer_s_set: '\sigma^{\mathrm{set}}' + Transformer_cycle_weight: '\mathrm{x}^{\sigma}' + Transformer_phase_shift_weight: '\vartheta' GlobalConstraint_type: '\mathrm{type}' GlobalConstraint_sense: '\mathrm{sense}' GlobalConstraint_constant: '\mathrm{K}' @@ -120,6 +154,7 @@ names: Generator_tech_capacity_weight: '\mathrm{m}' Link_tech_capacity_weight: '\mathrm{m}^{f}' Line_tech_capacity_weight: '\mathrm{m}^{l}' + Process_tech_capacity_weight: '\mathrm{m}^{z}' StorageUnit_tech_capacity_weight: '\mathrm{m}^{h}' Store_tech_capacity_weight: '\mathrm{m}^{e}' StorageUnit_p_dispatch: "h^{+}" @@ -168,11 +203,15 @@ names: StorageUnit_active: '\mathrm{on}^{h}' Store_active: '\mathrm{on}^{e}' Line_active: '\mathrm{on}^{s}' + Process_active: '\mathrm{on}^{z}' + Transformer_active: '\mathrm{on}^{\sigma}' Generator_capital_weight: '\mathrm{W}' Link_capital_weight: '\mathrm{W}^{f}' StorageUnit_capital_weight: '\mathrm{W}^{h}' Store_capital_weight: '\mathrm{W}^{e}' Line_capital_weight: '\mathrm{W}^{s}' + Process_capital_weight: '\mathrm{W}^{z}' + Transformer_capital_weight: '\mathrm{W}^{\sigma}' Generator_first_active: '\mathrm{new}' Carrier_max_growth: '\overline{\Delta}' Carrier_max_relative_growth: '\mathrm{r}' diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index 2c6ac7f8..cde00bcb 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -8,600 +8,735 @@ constraints: Bus_nodal_balance: - - snapshot - - bus + - snapshot + - bus Generator_com_down_time: - - snapshot - - generator + - snapshot + - generator Generator_com_ext_p_lower: - - snapshot - - generator + - snapshot + - generator Generator_com_ext_p_lower_nonneg: - - snapshot - - generator + - snapshot + - generator Generator_com_ext_p_upper_big_m: - - snapshot - - generator + - snapshot + - generator Generator_com_ext_p_upper_cap: - - snapshot - - generator + - snapshot + - generator Generator_com_mod_p_lower: - - snapshot - - generator + - snapshot + - generator Generator_com_mod_p_upper: - - snapshot - - generator + - snapshot + - generator Generator_com_p_lower: - - snapshot - - generator + - snapshot + - generator Generator_com_p_upper: - - snapshot - - generator + - snapshot + - generator Generator_com_status_must_stay_up: - - snapshot - - generator + - snapshot + - generator Generator_com_transition_shut_down: - - snapshot - - generator + - snapshot + - generator Generator_com_transition_start_up: - - snapshot - - generator + - snapshot + - generator Generator_com_up_time: - - snapshot - - generator + - snapshot + - generator Generator_e_sum_max: - - generator + - generator Generator_e_sum_min: - - generator + - generator Generator_ext_p_lower: - - snapshot - - generator + - snapshot + - generator Generator_ext_p_nom_lower: - - generator + - generator Generator_ext_p_nom_upper: - - generator + - generator Generator_ext_p_upper: - - snapshot - - generator + - snapshot + - generator Generator_fix_p_lower: - - snapshot - - generator + - snapshot + - generator Generator_fix_p_upper: - - snapshot - - generator + - snapshot + - generator Generator_p_nom_modularity: - - generator + - generator Generator_p_nom_set: - - generator + - generator Generator_p_ramp_limit_down: - - snapshot - - generator + - snapshot + - generator Generator_p_ramp_limit_down_run_big_m: - - snapshot - - generator + - snapshot + - generator Generator_p_ramp_limit_down_shut_big_m: - - snapshot - - generator + - snapshot + - generator Generator_p_ramp_limit_up: - - snapshot - - generator + - snapshot + - generator Generator_p_ramp_limit_up_run_big_m: - - snapshot - - generator + - snapshot + - generator Generator_p_ramp_limit_up_start_big_m: - - snapshot - - generator + - snapshot + - generator Generator_p_set: - - snapshot - - generator + - snapshot + - generator Generator_shut_down_p_fixed_upper: - - snapshot - - generator + - snapshot + - generator Generator_shut_down_p_nom_variable_upper: - - snapshot - - generator + - snapshot + - generator Generator_start_up_p_fixed_upper: - - snapshot - - generator + - snapshot + - generator Generator_start_up_p_nom_variable_upper: - - snapshot - - generator + - snapshot + - generator Generator_status_p_fixed_upper: - - snapshot - - generator + - snapshot + - generator Generator_status_p_nom_variable_upper: - - snapshot - - generator + - snapshot + - generator GlobalConstraint_operational_limit_eq: - - global_constraint + - global_constraint GlobalConstraint_operational_limit_lb: - - global_constraint + - global_constraint GlobalConstraint_operational_limit_ub: - - global_constraint + - global_constraint GlobalConstraint_primary_energy_eq: - - global_constraint + - global_constraint GlobalConstraint_primary_energy_lb: - - global_constraint + - global_constraint GlobalConstraint_primary_energy_ub: - - global_constraint + - global_constraint GlobalConstraint_tech_capacity_expansion_limit_eq: - - global_constraint + - global_constraint GlobalConstraint_tech_capacity_expansion_limit_lb: - - global_constraint + - global_constraint GlobalConstraint_tech_capacity_expansion_limit_ub: - - global_constraint + - global_constraint GlobalConstraint_transmission_expansion_cost_limit_eq: - - global_constraint + - global_constraint GlobalConstraint_transmission_expansion_cost_limit_lb: - - global_constraint + - global_constraint GlobalConstraint_transmission_expansion_cost_limit_ub: - - global_constraint + - global_constraint GlobalConstraint_transmission_volume_expansion_limit_eq: - - global_constraint + - global_constraint GlobalConstraint_transmission_volume_expansion_limit_lb: - - global_constraint + - global_constraint GlobalConstraint_transmission_volume_expansion_limit_ub: - - global_constraint + - global_constraint Kirchhoff_Voltage_Law: - - snapshot - - cycle + - snapshot + - cycle Line_ext_s_lower: - - snapshot - - line + - snapshot + - line Line_ext_s_nom_lower: - - line + - line Line_ext_s_nom_upper: - - line + - line Line_ext_s_upper: - - snapshot - - line + - snapshot + - line Line_fix_s_lower: - - snapshot - - line + - snapshot + - line Line_fix_s_upper: - - snapshot - - line + - snapshot + - line Line_s_nom_set: - - line + - line Line_s_set: - - snapshot - - line + - snapshot + - line Link_ext_p_lower: - - snapshot - - link + - snapshot + - link Link_ext_p_nom_lower: - - link + - link Link_ext_p_nom_upper: - - link + - link Link_ext_p_upper: - - snapshot - - link + - snapshot + - link Link_fix_p_lower: - - snapshot - - link + - snapshot + - link Link_fix_p_upper: - - snapshot - - link + - snapshot + - link Link_p_nom_set: - - link + - link Link_p_ramp_limit_down: - - snapshot - - link + - snapshot + - link Link_p_ramp_limit_up: - - snapshot - - link + - snapshot + - link Link_p_set: - - snapshot - - link + - snapshot + - link + Process_ext_p_lower: + - snapshot + - process + Process_ext_p_nom_lower: + - process + Process_ext_p_nom_upper: + - process + Process_ext_p_upper: + - snapshot + - process + Process_fix_p_lower: + - snapshot + - process + Process_fix_p_upper: + - snapshot + - process + Process_p_nom_set: + - process + Process_p_ramp_limit_down: + - snapshot + - process + Process_p_ramp_limit_up: + - snapshot + - process + Process_p_set: + - snapshot + - process StorageUnit_energy_balance: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_ext_p_dispatch_lower: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_ext_p_dispatch_upper: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_ext_p_nom_lower: - - storage_unit + - storage_unit StorageUnit_ext_p_nom_upper: - - storage_unit + - storage_unit StorageUnit_ext_p_store_lower: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_ext_p_store_upper: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_ext_state_of_charge_lower: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_ext_state_of_charge_upper: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_fix_p_dispatch_lower: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_fix_p_dispatch_upper: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_fix_p_store_lower: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_fix_p_store_upper: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_fix_state_of_charge_lower: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_fix_state_of_charge_upper: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_p_nom_set: - - storage_unit + - storage_unit StorageUnit_p_set: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_state_of_charge_set: - - snapshot - - storage_unit + - snapshot + - storage_unit Store_e_nom_set: - - store + - store Store_e_set: - - snapshot - - store + - snapshot + - store Store_energy_balance: - - snapshot - - store + - snapshot + - store Store_ext_e_lower: - - snapshot - - store + - snapshot + - store Store_ext_e_nom_lower: - - store + - store Store_ext_e_nom_upper: - - store + - store Store_ext_e_upper: - - snapshot - - store + - snapshot + - store Store_fix_e_lower: - - snapshot - - store + - snapshot + - store Store_fix_e_upper: - - snapshot - - store + - snapshot + - store + Transformer_ext_s_lower: + - snapshot + - transformer + Transformer_ext_s_nom_lower: + - transformer + Transformer_ext_s_nom_upper: + - transformer + Transformer_ext_s_upper: + - snapshot + - transformer + Transformer_fix_s_lower: + - snapshot + - transformer + Transformer_fix_s_upper: + - snapshot + - transformer + Transformer_s_nom_set: + - transformer + Transformer_s_set: + - snapshot + - transformer expressions: -- Generator_p_nom_effective -- Generator_previous_p -- Generator_previous_status -- Generator_ramp_down_allowance -- Generator_ramp_up_allowance -- Link_output_arrival -- Link_p_nom_effective -- StorageUnit_charge_carried_in -- Store_energy_carried_in -- operational_limit -- primary_energy -- tech_capacity_expansion -- transmission_expansion_cost -- transmission_volume_expansion + - Generator_p_nom_effective + - Generator_previous_p + - Generator_previous_status + - Generator_ramp_down_allowance + - Generator_ramp_up_allowance + - Link_output_arrival + - Link_p_nom_effective + - Process_output_arrival + - Process_p_nom_effective + - StorageUnit_charge_carried_in + - Store_energy_carried_in + - operational_limit + - primary_energy + - tech_capacity_expansion + - transmission_expansion_cost + - transmission_volume_expansion parameters: Generator_big_m: - - generator + - generator Generator_capital_cost: - - generator + - generator Generator_committable: - - generator + - generator Generator_e_sum_max: - - generator + - generator Generator_e_sum_min: - - generator + - generator Generator_marginal_cost: - - snapshot - - generator + - snapshot + - generator Generator_min_down_time: - - generator + - generator Generator_min_up_time: - - generator + - generator Generator_modules_installed: - - generator + - generator Generator_must_stay_up: - - snapshot - - generator + - snapshot + - generator Generator_operational_limit_weight: - - global_constraint - - generator + - global_constraint + - generator Generator_p_max_pu: - - snapshot - - generator + - snapshot + - generator Generator_p_min_pu: - - snapshot - - generator + - snapshot + - generator Generator_p_min_pu_nonneg: - - generator + - generator Generator_p_nom: - - generator + - generator Generator_p_nom_extendable: - - generator + - generator Generator_p_nom_max: - - generator + - generator Generator_p_nom_min: - - generator + - generator Generator_p_nom_mod: - - generator + - generator Generator_p_nom_set: - - generator + - generator Generator_p_set: - - snapshot - - generator + - snapshot + - generator Generator_primary_energy_weight: - - global_constraint - - generator + - global_constraint + - generator Generator_ramp_limit_down: - - generator + - generator Generator_ramp_limit_shut_down: - - generator + - generator Generator_ramp_limit_start_up: - - generator + - generator Generator_ramp_limit_up: - - generator + - generator Generator_shut_down_cost: - - generator + - generator Generator_stand_by_cost: - - snapshot - - generator + - snapshot + - generator Generator_start_up_cost: - - generator + - generator Generator_status_initial: - - generator + - generator Generator_tech_capacity_weight: - - global_constraint - - generator + - global_constraint + - generator GlobalConstraint_constant: - - global_constraint + - global_constraint GlobalConstraint_sense: - - global_constraint + - global_constraint GlobalConstraint_type: - - global_constraint + - global_constraint Line_capital_cost: - - line + - line Line_cycle_weight: - - line - - cycle + - line + - cycle Line_expansion_cost_weight: - - global_constraint - - line + - global_constraint + - line Line_s_max_pu: - - snapshot - - line + - snapshot + - line Line_s_nom: - - line + - line Line_s_nom_extendable: - - line + - line Line_s_nom_max: - - line + - line Line_s_nom_min: - - line + - line Line_s_nom_set: - - line + - line Line_s_set: - - snapshot - - line + - snapshot + - line Line_tech_capacity_weight: - - global_constraint - - line + - global_constraint + - line Line_volume_weight: - - global_constraint - - line + - global_constraint + - line Link_capital_cost: - - link + - link Link_efficiency: - - link_output + - link_output Link_expansion_cost_weight: - - global_constraint - - link + - global_constraint + - link Link_marginal_cost: - - snapshot - - link + - snapshot + - link Link_output_cyclic_delay: - - link_output + - link_output Link_output_delay: - - link_output + - link_output Link_p_max_pu: - - snapshot - - link + - snapshot + - link Link_p_min_pu: - - snapshot - - link + - snapshot + - link Link_p_nom: - - link + - link Link_p_nom_extendable: - - link + - link Link_p_nom_max: - - link + - link Link_p_nom_min: - - link + - link Link_p_nom_set: - - link + - link Link_p_set: - - snapshot - - link + - snapshot + - link Link_ramp_limit_down: - - link + - link Link_ramp_limit_up: - - link + - link Link_tech_capacity_weight: - - global_constraint - - link + - global_constraint + - link Link_volume_weight: - - global_constraint - - link + - global_constraint + - link Load_p_set: - - snapshot - - load + - snapshot + - load + Process_active: + - snapshot + - process + Process_capital_cost: + - process + Process_capital_weight: + - process + Process_marginal_cost: + - snapshot + - process + Process_output_cyclic_delay: + - process_output + Process_output_delay: + - process_output + Process_p_max_pu: + - snapshot + - process + Process_p_min_pu: + - snapshot + - process + Process_p_nom: + - process + Process_p_nom_extendable: + - process + Process_p_nom_max: + - process + Process_p_nom_min: + - process + Process_p_nom_set: + - process + Process_p_set: + - snapshot + - process + Process_ramp_limit_down: + - process + Process_ramp_limit_up: + - process + Process_rate: + - process_output + Process_tech_capacity_weight: + - global_constraint + - process StorageUnit_capital_cost: - - storage_unit + - storage_unit StorageUnit_cyclic_state_of_charge: - - storage_unit + - storage_unit StorageUnit_efficiency_dispatch: - - storage_unit + - storage_unit StorageUnit_efficiency_store: - - storage_unit + - storage_unit StorageUnit_inflow: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_marginal_cost: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_marginal_cost_storage: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_max_hours: - - storage_unit + - storage_unit StorageUnit_operational_limit_weight: - - global_constraint - - storage_unit + - global_constraint + - storage_unit StorageUnit_p_max_pu: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_p_min_pu: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_p_nom: - - storage_unit + - storage_unit StorageUnit_p_nom_extendable: - - storage_unit + - storage_unit StorageUnit_p_nom_max: - - storage_unit + - storage_unit StorageUnit_p_nom_min: - - storage_unit + - storage_unit StorageUnit_p_nom_set: - - storage_unit + - storage_unit StorageUnit_p_set: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_primary_energy_weight: - - global_constraint - - storage_unit + - global_constraint + - storage_unit StorageUnit_retention: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_spill_cost: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_state_of_charge_initial: - - storage_unit + - storage_unit StorageUnit_state_of_charge_set: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_tech_capacity_weight: - - global_constraint - - storage_unit + - global_constraint + - storage_unit Store_capital_cost: - - store + - store Store_e_cyclic: - - store + - store Store_e_initial: - - store + - store Store_e_max_pu: - - snapshot - - store + - snapshot + - store Store_e_min_pu: - - snapshot - - store + - snapshot + - store Store_e_nom: - - store + - store Store_e_nom_extendable: - - store + - store Store_e_nom_max: - - store + - store Store_e_nom_min: - - store + - store Store_e_nom_set: - - store + - store Store_e_set: - - snapshot - - store + - snapshot + - store Store_marginal_cost: - - snapshot - - store + - snapshot + - store Store_marginal_cost_storage: - - snapshot - - store + - snapshot + - store Store_operational_limit_weight: - - global_constraint - - store + - global_constraint + - store Store_primary_energy_weight: - - global_constraint - - store + - global_constraint + - store Store_retention: - - snapshot - - store + - snapshot + - store Store_tech_capacity_weight: - - global_constraint - - store + - global_constraint + - store + Transformer_active: + - snapshot + - transformer + Transformer_capital_cost: + - transformer + Transformer_capital_weight: + - transformer + Transformer_cycle_weight: + - transformer + - cycle + Transformer_phase_shift_weight: + - transformer + - cycle + Transformer_s_max_pu: + - snapshot + - transformer + Transformer_s_nom: + - transformer + Transformer_s_nom_extendable: + - transformer + Transformer_s_nom_max: + - transformer + Transformer_s_nom_min: + - transformer + Transformer_s_nom_set: + - transformer + Transformer_s_set: + - snapshot + - transformer snapshot_is_last: - - snapshot + - snapshot snapshot_weightings_generators: - - snapshot + - snapshot snapshot_weightings_objective: - - snapshot + - snapshot snapshot_weightings_stores: - - snapshot + - snapshot relations: -- Generator_bus -- Line_bus0 -- Line_bus1 -- Link_bus0 -- Link_output_bus -- Link_output_link -- Load_bus -- StorageUnit_bus -- Store_bus + - Generator_bus + - Line_bus0 + - Line_bus1 + - Link_bus0 + - Link_output_bus + - Link_output_link + - Load_bus + - Process_output_bus + - Process_output_process + - StorageUnit_bus + - Store_bus + - Transformer_bus0 + - Transformer_bus1 variables: Generator_n_mod: - - generator + - generator Generator_p: - - snapshot - - generator + - snapshot + - generator Generator_p_nom_ext: - - generator + - generator Generator_shut_down: - - snapshot - - generator + - snapshot + - generator Generator_start_up: - - snapshot - - generator + - snapshot + - generator Generator_status: - - snapshot - - generator + - snapshot + - generator Line_s: - - snapshot - - line + - snapshot + - line Line_s_nom_ext: - - line + - line Link_p: - - snapshot - - link + - snapshot + - link Link_p_nom_ext: - - link + - link + Process_p: + - snapshot + - process + Process_p_nom_ext: + - process StorageUnit_p_dispatch: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_p_nom_ext: - - storage_unit + - storage_unit StorageUnit_p_store: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_spill: - - snapshot - - storage_unit + - snapshot + - storage_unit StorageUnit_state_of_charge: - - snapshot - - storage_unit + - snapshot + - storage_unit Store_e: - - snapshot - - store + - snapshot + - store Store_e_nom_ext: - - store + - store Store_p: - - snapshot - - store + - snapshot + - store + Transformer_s: + - snapshot + - transformer + Transformer_s_nom_ext: + - transformer diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index 8f7ef633..17af5401 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -32,6 +32,8 @@ 'StorageUnit_p_nom_ext', 'Store_e_nom_ext', 'Line_s_nom_ext', + 'Process_p_nom_ext', + 'Transformer_s_nom_ext', } From 4c042cdf85e458380b9f7073540004ea104831f7 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 22 Sep 2026 20:14:17 +0200 Subject: [PATCH 03/46] docs(pypsa): the spec covers quadratic marginal cost --- .prettierignore | 1 - docs/examples/pypsa.md | 61 ++++- docs/examples/pypsa_quadratic.md | 217 ------------------ examples/pypsa.yaml | 8 + examples/pypsa_quadratic.yaml | 149 ------------ .../pypsa/rung_10_quadratic_costs.py | 5 +- examples/symbols/pypsa.yaml | 2 + examples/symbols/pypsa_quadratic.yaml | 30 --- mkdocs.yml | 1 - tests/fixtures/pypsa_standard_shape.yaml | 6 + tools/gallery.py | 1 - 11 files changed, 76 insertions(+), 405 deletions(-) delete mode 100644 docs/examples/pypsa_quadratic.md delete mode 100644 examples/pypsa_quadratic.yaml delete mode 100644 examples/symbols/pypsa_quadratic.yaml diff --git a/.prettierignore b/.prettierignore index 1aa182c8..4cdba9bd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,7 +24,6 @@ docs/examples/dispatch.md docs/examples/commitment.md docs/examples/operators.md docs/examples/pypsa.md -docs/examples/pypsa_quadratic.md docs/examples/pypsa_linearized_uc.md docs/examples/pypsa_losses.md diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index b386864f..7acdd7ed 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -86,7 +86,7 @@ def build(): | [`Bus-nodal_balance`](#bus-nodal_balance) | done | a loaded bus with nothing attached: PyPSA refuses, see X2 | | `Bus-meshed-*-nodal_balance` | out | the same balance rows, dealt into linopy containers by how many component columns name a bus — `meshed_thresholds`, an `n.optimize()` keyword defaulting to `[30, 100, 400]`. Same rows, same duals, another name; a modeler whose engine wants the split states it, the file does not (#123) | | [`marginal_cost`](#objective) | done | | -| [`marginal_cost_quadratic`](pypsa_quadratic.md) | done | rung 10, a file of its own | +| [`marginal_cost_quadratic`](#objective) | done | rung 10, below; Generator and Link — PyPSA also carries it on storage units and stores, one more term each of the same shape | | `objective_constant` | split | an objective shift, compared net of `n._objective_constant` — rungs 11 and 13 carry a nonzero one, `21915277.52` and `160.0`, so the netting is under test | @@ -811,6 +811,59 @@ def build(): +### Rung 10 — quadratic costs + +A marginal cost quadratic in output: PyPSA's `marginal_cost_quadratic`, one +squared term per component in the objective, each snapshot weighted by the hours +it stands for. Generator and Link carry it here; PyPSA also carries it on +storage units and stores, one more term each of the same shape. A plain run +feeds zero, so the term vanishes and the objective stays linear. + +| PyPSA | status | note | +| --- | --- | --- | +| [`marginal_cost_quadratic`](#objective) | done | degree 2 in the objective; Generator and Link here | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `12587.437500000098`, 60 rows. + +
+The network, as PyPSA code + +`rung_10_quadratic_costs.py` + +```python +"""Rung 10: quadratic costs — a marginal cost quadratic in output.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'village') + n.add('Generator', 'steam', bus='north', p_nom=80, marginal_cost=5, marginal_cost_quadratic=0.08) + n.add('Generator', 'engine', bus='north', p_nom=80, marginal_cost=20, marginal_cost_quadratic=0.01) + n.add( + 'Link', + 'wire2', + bus0='north', + bus1='village', + p_nom=40, + p_min_pu=-1, + efficiency=0.9, + marginal_cost=1, + marginal_cost_quadratic=0.02, + ) + n.add('Load', 'village_load', bus='village', p_set=15) + n.add('Load', 'extra10', bus='north', p_set=[30, 50, 40, 60]) + return n +``` + +
+ + ### Rung 11 — ac-dc-meshed PyPSA's `ac_dc_meshed` example, whole: meshed AC and DC, extendable lines, @@ -1577,6 +1630,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | | $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | | $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | +| $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | | $`\mathrm{com}`$ | `Generator_committable` over $`\mathcal{G}`$ — whether output is gated by an on/off status decision | | $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit | | $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit | @@ -1603,6 +1657,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once | | $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the horizon's end — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at the first snapshots is lost | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | +| $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | | $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\mathcal{J}`$ — nominal internal power | | $`\mathrm{ext}^{z}`$ | `Process_p_nom_extendable` over $`\mathcal{J}`$ — whether the nominal internal power is a decision | | $`\underline{\mathrm{z}}`$ | `Process_p_min_pu` over $`\mathcal{T} \times \mathcal{J}`$ — least internal power, per unit of nominal power — negative for a process that runs both ways | @@ -4146,7 +4201,9 @@ scenario_opex: description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight expression: >- sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_p * Generator_p * Generator_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_p * Link_p * Link_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Process_p * Process_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) @@ -4159,7 +4216,7 @@ scenario_opex: ``` ```math -\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi ``` #### Variable domains diff --git a/docs/examples/pypsa_quadratic.md b/docs/examples/pypsa_quadratic.md deleted file mode 100644 index 866e7e3f..00000000 --- a/docs/examples/pypsa_quadratic.md +++ /dev/null @@ -1,217 +0,0 @@ - - -# PyPSA, the quadratic class - -Rung 10 of [PyPSA in one file](pypsa.md): PyPSA's `marginal_cost_quadratic`, -stated on rung 1's transport surface in a file of its own. Its network is the -shared spine, shown once on [the rung ladder's page](pypsa.md#index). - -## Rung 10 — quadratic costs - -| PyPSA | status | note | -| --------------------------------------- | ------ | -------------------------------------------------------------------------- | -| [`marginal_cost_quadratic`](#objective) | done | degree 2 in the objective; Generator and Link here — PyPSA also carries it on storage units and stores, one more term each of the same shape | - - -> ✔ `pypsa 1.3.0` solves this rung's network at objective `12587.437500000098`, 60 rows. - -
-The network, as PyPSA code - -`rung_10_quadratic_costs.py` - -```python -"""Rung 10: quadratic costs — a marginal cost quadratic in output, stated by `pypsa_quadratic.yaml`.""" - -from __future__ import annotations - -import spine - -#: This rung binds a file of its own. -MODEL = 'pypsa_quadratic.yaml' - - -def build(): - """The spine plus this rung's additions, as a ``pypsa.Network``.""" - n = spine.build() - n.add('Bus', 'village') - n.add('Generator', 'steam', bus='north', p_nom=80, marginal_cost=5, marginal_cost_quadratic=0.08) - n.add('Generator', 'engine', bus='north', p_nom=80, marginal_cost=20, marginal_cost_quadratic=0.01) - n.add( - 'Link', - 'wire2', - bus0='north', - bus1='village', - p_nom=40, - p_min_pu=-1, - efficiency=0.9, - marginal_cost=1, - marginal_cost_quadratic=0.02, - ) - n.add('Load', 'village_load', bus='village', p_set=15) - n.add('Load', 'extra10', bus='north', p_set=[30, 50, 40, 60]) - return n -``` - -
- - -## The file - - -The quadratic class of a plain `n.optimize()`: PyPSA's `marginal_cost_quadratic`, stated on rung 1's transport surface in a file of its own. One model cannot carry a quadratic objective beside commitment's integer variables and still solve on HiGHS, because degree is the model's property and not the data's. So the class a free solver takes as a QP lives here, and `examples/pypsa.yaml` stays the mixed-integer one. PyPSA also carries the attribute on storage units and stores; each is one more term of the same shape. - -#### Sets - -| Symbol | Meaning | -|---|---| -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | - -#### Parameters - -| Symbol | Meaning | -|---|---| -| $`\mathrm{w}`$ | `snapshot_weightings_objective` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost | -| $`\mathrm{p}^{\mathrm{nom}}`$ | `Generator_p_nom` over $`\mathcal{G}`$ — nominal power | -| $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | -| $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | -| $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | -| $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | -| $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\mathcal{L}`$ — nominal power | -| $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | -| $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | -| $`\eta`$ | `Link_efficiency` over $`\mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | -| $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | -| $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | -| $`\mathrm{load}`$ | `Load_p_set` over $`\mathcal{T} \times \mathcal{D}`$ — demand | - -#### Variables - -| Symbol | Meaning | -|---|---| -| $`p`$ | `Generator_p` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-p` — output of a generator in a snapshot | -| $`f`$ | `Link_p` over $`\mathcal{T} \times \mathcal{L}`$ — `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to | - -### Objective - -```yaml -objective: - sense: minimize - description: operating cost, linear and quadratic, each snapshot weighted by the hours it stands for - expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective) - + sum(Generator_p * Generator_p * Generator_marginal_cost_quadratic * snapshot_weightings_objective) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective) - + sum(Link_p * Link_p * Link_marginal_cost_quadratic * snapshot_weightings_objective) -``` - -```math -\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ l \in \mathcal{L}} f_{t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ l \in \mathcal{L}} f_{t,l} \cdot f_{t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} -``` - -### `Generator-fix-p-lower` - -`Generator_fix_p_lower` - -```yaml -Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [snapshot, generator] - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom -``` - -```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} -``` - -### `Generator-fix-p-upper` - -`Generator_fix_p_upper` - -```yaml -Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [snapshot, generator] - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom -``` - -```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} -``` - -### `Link-fix-p-lower` - -`Link_fix_p_lower` - -```yaml -Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom -``` - -```math -f_{t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Link-fix-p-upper` - -`Link_fix_p_upper` - -```yaml -Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom -``` - -```math -f_{t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Bus-nodal_balance` - -`Bus_nodal_balance` - -```yaml -Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, less what the links - take away, plus what arrives over them after losses, meets the load - there - dims: [snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) -``` - -```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{t,g} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} f_{t,\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} -``` - -#### Variable domains - -**`Generator_p`** - -```math -p_{t,g} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} -``` - -**`Link_p`** - -```math -f_{t,l} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - - -Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 4e6f7a2c..591b4fc6 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -77,6 +77,9 @@ parameters: Generator_marginal_cost: description: cost of one unit of output dims: [snapshot, generator] + Generator_marginal_cost_quadratic: + description: cost of the square of one unit of output + dims: [snapshot, generator] Generator_committable: description: whether output is gated by an on/off status decision dims: [generator] @@ -182,6 +185,9 @@ parameters: Link_marginal_cost: description: cost of one unit of flow dims: [snapshot, link] + Link_marginal_cost_quadratic: + description: cost of the square of one unit of flow + dims: [snapshot, link] Process_p_nom: description: nominal internal power dims: [process] @@ -984,7 +990,9 @@ expressions: description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight expression: >- sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_p * Generator_p * Generator_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_p * Link_p * Link_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Process_p * Process_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) diff --git a/examples/pypsa_quadratic.yaml b/examples/pypsa_quadratic.yaml deleted file mode 100644 index e8c665a9..00000000 --- a/examples/pypsa_quadratic.yaml +++ /dev/null @@ -1,149 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -description: >- - The quadratic class of a plain `n.optimize()`: PyPSA's - `marginal_cost_quadratic`, stated on rung 1's transport surface in a file of - its own. One model cannot carry a quadratic objective beside commitment's - integer variables and still solve on HiGHS, because degree is the model's - property and not the data's. So the class a free solver takes as a QP lives - here, and `examples/pypsa.yaml` stays the mixed-integer one. PyPSA also carries the - attribute on storage units and stores; each is one more term of the same - shape. - -dimensions: - snapshot: - description: dispatch periods - dtype: datetime - bus: - description: network nodes - generator: - description: generating units, each on one bus - link: - description: controllable connections, each from one bus to the buses it delivers to - link_output: - description: >- - a link's output ports, one label per port a link declares — PyPSA's - `bus1`, `bus2`, … columns read long, so a link of any number of output - ports is one term in the balance, data prep - load: - description: demands, each on one bus - -parameters: - snapshot_weightings_objective: - description: PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost - dims: [snapshot] - Generator_p_nom: - description: nominal power - dims: [generator] - Generator_p_min_pu: - description: least output, per unit of nominal power - dims: [snapshot, generator] - Generator_p_max_pu: - description: most output, per unit of nominal power — an availability profile - dims: [snapshot, generator] - Generator_marginal_cost: - description: cost of one unit of output - dims: [snapshot, generator] - Generator_marginal_cost_quadratic: - description: cost of the square of one unit of output - dims: [snapshot, generator] - Link_p_nom: - description: nominal power - dims: [link] - Link_p_min_pu: - description: least flow, per unit of nominal power — negative for a link that carries both ways - dims: [snapshot, link] - Link_p_max_pu: - description: most flow, per unit of nominal power - dims: [snapshot, link] - Link_efficiency: - description: >- - share of the flow that arrives at an output port, PyPSA's `efficiency`, - `efficiency2`, … read long — negative where that port consumes rather - than delivers - dims: [link_output] - Link_marginal_cost: - description: cost of one unit of flow - dims: [snapshot, link] - Link_marginal_cost_quadratic: - description: cost of the square of one unit of flow - dims: [snapshot, link] - Load_p_set: - description: demand - dims: [snapshot, load] - -relations: - Generator_bus: - description: the bus a generator sits on - key: generator - values: bus - Link_bus0: - description: the bus a link leaves - key: link - values: bus - Link_output_link: - description: the link an output port belongs to - key: link_output - values: link - Link_output_bus: - description: >- - the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. - A link of three output ports is three labels here rather than a third - relation, so the file states any number of them - key: link_output - values: bus - Load_bus: - description: the bus a load sits on - key: load - values: bus - -variables: - Generator_p: - description: "`Generator-p` — output of a generator in a snapshot" - dims: [snapshot, generator] - Link_p: - description: >- - `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a - positive value withdraws there and injects at every bus the link's - output ports deliver to - dims: [snapshot, link] - -constraints: - Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [snapshot, generator] - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom - Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [snapshot, generator] - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom - Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom - Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom - Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, less what the links - take away, plus what arrives over them after losses, meets the load - there - dims: [snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) - -objective: - sense: minimize - description: operating cost, linear and quadratic, each snapshot weighted by the hours it stands for - expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective) - + sum(Generator_p * Generator_p * Generator_marginal_cost_quadratic * snapshot_weightings_objective) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective) - + sum(Link_p * Link_p * Link_marginal_cost_quadratic * snapshot_weightings_objective) diff --git a/examples/references/pypsa/rung_10_quadratic_costs.py b/examples/references/pypsa/rung_10_quadratic_costs.py index c4b37d77..c8d851f6 100644 --- a/examples/references/pypsa/rung_10_quadratic_costs.py +++ b/examples/references/pypsa/rung_10_quadratic_costs.py @@ -1,12 +1,9 @@ -"""Rung 10: quadratic costs — a marginal cost quadratic in output, stated by `pypsa_quadratic.yaml`.""" +"""Rung 10: quadratic costs — a marginal cost quadratic in output.""" from __future__ import annotations import spine -#: This rung binds a file of its own. -MODEL = 'pypsa_quadratic.yaml' - def build(): """The spine plus this rung's additions, as a ``pypsa.Network``.""" diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 07f01892..269b1c21 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -30,6 +30,7 @@ names: Generator_p_min_pu: '\underline{\mathrm{p}}' Generator_p_max_pu: '\overline{\mathrm{p}}' Generator_marginal_cost: '\mathrm{c}' + Generator_marginal_cost_quadratic: '\mathrm{c}^{(2)}' Generator_committable: '\mathrm{com}' Generator_ramp_limit_up: '\mathrm{ru}' Generator_ramp_limit_down: '\mathrm{rd}' @@ -65,6 +66,7 @@ names: Link_output_delay: '\mathrm{d}^{f}' Link_output_cyclic_delay: '\mathrm{cyc}^{f}' Link_marginal_cost: '\mathrm{c}^{f}' + Link_marginal_cost_quadratic: '\mathrm{c}^{f,(2)}' Load_p_set: '\mathrm{load}' Generator_p_set: '\mathrm{p}^{\mathrm{set}}' Link_p_set: '\mathrm{f}^{\mathrm{set}}' diff --git a/examples/symbols/pypsa_quadratic.yaml b/examples/symbols/pypsa_quadratic.yaml deleted file mode 100644 index 119bcdc3..00000000 --- a/examples/symbols/pypsa_quadratic.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -# How `examples/pypsa_quadratic.yaml` prints — the rung 1 symbols, plus the -# quadratic coefficients. -notation: latex -dimensions: - snapshot: { index: t, set: '\mathcal{T}' } - bus: { index: n, set: '\mathcal{N}' } - generator: { index: g, set: '\mathcal{G}' } - link: { index: l, set: '\mathcal{L}' } - link_output: { index: o, set: '\mathcal{O}' } - load: { index: d, set: '\mathcal{D}' } -names: - snapshot_weightings_objective: '\mathrm{w}' - Generator_p: p - Generator_p_nom: '\mathrm{p}^{\mathrm{nom}}' - Generator_p_min_pu: '\underline{\mathrm{p}}' - Generator_p_max_pu: '\overline{\mathrm{p}}' - Generator_marginal_cost: '\mathrm{c}' - Generator_marginal_cost_quadratic: '\mathrm{c}^{(2)}' - Link_p: f - Link_p_nom: '\mathrm{f}^{\mathrm{nom}}' - Link_p_min_pu: '\underline{\mathrm{f}}' - Link_p_max_pu: '\overline{\mathrm{f}}' - Link_efficiency: '\eta' - Link_marginal_cost: '\mathrm{c}^{f}' - Link_marginal_cost_quadratic: '\mathrm{c}^{f,(2)}' - Load_p_set: '\mathrm{load}' diff --git a/mkdocs.yml b/mkdocs.yml index 33d2c00b..acdf2310 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -63,7 +63,6 @@ nav: - Unit commitment: examples/commitment.md - One construct per model: examples/operators.md - PyPSA in one file: examples/pypsa.md - - PyPSA, the quadratic class: examples/pypsa_quadratic.md - PyPSA, the relaxed commitment: examples/pypsa_linearized_uc.md - PyPSA, the lossy lines: examples/pypsa_losses.md # Everything a reader does not need in order to write a model: the design diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index cde00bcb..c93d05a4 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -347,6 +347,9 @@ parameters: Generator_marginal_cost: - snapshot - generator + Generator_marginal_cost_quadratic: + - snapshot + - generator Generator_min_down_time: - generator Generator_min_up_time: @@ -451,6 +454,9 @@ parameters: Link_marginal_cost: - snapshot - link + Link_marginal_cost_quadratic: + - snapshot + - link Link_output_cyclic_delay: - link_output Link_output_delay: diff --git a/tools/gallery.py b/tools/gallery.py index de078b7a..fc36a48c 100644 --- a/tools/gallery.py +++ b/tools/gallery.py @@ -44,7 +44,6 @@ #: the declaration's own description. DECLARED = { 'pypsa.md': ROOT / 'examples' / 'pypsa.yaml', - 'pypsa_quadratic.md': ROOT / 'examples' / 'pypsa_quadratic.yaml', 'pypsa_linearized_uc.md': ROOT / 'examples' / 'pypsa_linearized_uc.yaml', 'pypsa_losses.md': ROOT / 'examples' / 'pypsa_losses.yaml', } From dc0ec065d739027d96624983d037f878dde69f95 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 22 Sep 2026 20:36:56 +0200 Subject: [PATCH 04/46] docs(pypsa): the spec covers transmission losses --- .prettierignore | 1 - docs/examples/pypsa.md | 260 +++++++++--- docs/examples/pypsa_losses.md | 426 -------------------- examples/pypsa.yaml | 68 +++- examples/pypsa_losses.yaml | 260 ------------ examples/references/pypsa/rung_13_losses.py | 3 +- examples/symbols/pypsa.yaml | 5 + examples/symbols/pypsa_losses.yaml | 43 -- mkdocs.yml | 3 +- tests/fixtures/pypsa_standard_shape.yaml | 12 + tools/gallery.py | 1 - 11 files changed, 273 insertions(+), 809 deletions(-) delete mode 100644 docs/examples/pypsa_losses.md delete mode 100644 examples/pypsa_losses.yaml delete mode 100644 examples/symbols/pypsa_losses.yaml diff --git a/.prettierignore b/.prettierignore index 4cdba9bd..6456f465 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,7 +25,6 @@ docs/examples/commitment.md docs/examples/operators.md docs/examples/pypsa.md docs/examples/pypsa_linearized_uc.md -docs/examples/pypsa_losses.md # The PyPSA reference scripts write this file; prettier would reformat what # they stamp, and the two would fight over it exactly as above. diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 7acdd7ed..94b1f84b 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1153,6 +1153,76 @@ def build(): +### Rung 13 — transmission losses + +`n.optimize(transmission_losses={'mode': 'tangents', 'segments': K})`: a line +dissipates a loss its flow buys along a fan of `K` tangents to the quadratic +loss curve, half charged at either end of the line. The loss variable, its cap +and the tangent rows exist only where `transmission_losses` is on; the tangent +slopes and offsets are data prep, one per `segment`. A plain run leaves the flag +off and supplies no segments, so the loss is absent and reads as zero in the +balance, and the model collapses to the lossless one. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Line-loss`](#variable-domains) | done | absent, and zero in the balance, where lossless | +| [`Line-fix-s-*`, `Line-ext-s-*`](#line-fix-s-lower) | done | the loss counted against the rating | +| [`Bus-nodal_balance`](#bus-nodal_balance) | done | half of each incident line's loss at either end | +| [`Line-loss_upper`](#line-loss_upper) | done | `loss_max` is data prep | +| [`Line-loss_tangents-{k}-1`](#line-loss_tangents-k-1) | split | PyPSA names a row per segment; one block over the dimension | +| [`Line-loss_tangents-{k}--1`](#line-loss_tangents-k--1) | split | | +| `Line-loss_secants-*` | out | the secant mode solves for its segment count | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `10645.295879552297`, 150 rows. + +
+The network, as PyPSA code + +`rung_13_losses.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 13: transmission losses in tangent form — a loss per line.""" + +from __future__ import annotations + +import spine + +OPTIMIZE = {'transmission_losses': {'mode': 'tangents', 'segments': 2}} + + +def build(): + """The spine plus a 110 kV triangle of lines, one of them extendable — ohms a real line has, so the loss stays a few percent of the flow.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c'], v_nom=110) + n.add('Generator', 'hydro13', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel13', bus='b', p_nom=80, marginal_cost=50) + n.add('Line', 'ab13', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) + n.add('Line', 'bc13', bus0='b', bus1='c', carrier='AC', x=60, r=9.7, s_nom=60) + n.add( + 'Line', + 'ca13', + bus0='c', + bus1='a', + carrier='AC', + x=45, + r=6, + s_nom=40, + s_nom_extendable=True, + s_nom_max=90, + capital_cost=4, + ) + n.add('Load', 'town13', bus='c', p_set=[35, 55, 15, 45]) + return n +``` + +
+ + ### Rung 14 — two-stage stochastic Two futures and a risk preference: `n.set_scenarios(...)` with @@ -1568,12 +1638,6 @@ def build(): -### Not on a rung - -| PyPSA | status | note | -| ------------------------------ | ------ | ------------------------------------ | -| [`{c}-loss*`](pypsa_losses.md) | done | rung 13, a file of its own; tangent form | - ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -1616,7 +1680,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | | $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a fixed phase shift | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | -| $`\mathcal{B}`$ | index $`b`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | +| $`\mathcal{B}`$ | index $`b`$ — `segment` — the tangents a line's loss curve is approximated by — PyPSA's `transmission_losses` count; none in a lossless run | +| $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | | $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | | $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | @@ -1752,6 +1817,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | | $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | | $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row | +| $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` count read as a flag, its tangents the `segment` axis; false with no segments is a lossless run | +| $`\overline{\ell}`$ | `Line_loss_max` over $`\mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | +| $`\mathrm{a}`$ | `Line_loss_slope` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a tangent to the loss curve at its segment's flow — `2 * r_pu_eff * p_k`, data prep | +| $`\mathrm{b}`$ | `Line_loss_offset` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — where that tangent meets the loss axis — `loss_k - slope_k * p_k`, negative, data prep | | $`\sigma^{\mathrm{nom}}`$ | `Transformer_s_nom` over $`\mathcal{M}`$ — nominal apparent power | | $`\mathrm{ext}^{\sigma}`$ | `Transformer_s_nom_extendable` over $`\mathcal{M}`$ — whether the nominal apparent power is a decision | | $`\overline{\sigma}`$ | `Transformer_s_max_pu` over $`\mathcal{T} \times \mathcal{M}`$ — most flow either way, per unit of nominal apparent power | @@ -1762,26 +1831,26 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | | $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row | | $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's fixed phase shift in radians, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; a transformer with no shift or in no cycle has no row. PyPSA also admits an optimisable phase shift, a later rung this file does not carry | -| $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{B}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | -| $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\mathcal{B}`$ — which way the row binds — `<=`, `>=` or `==` | -| $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{B}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, a non-extendable build — is folded in here by data prep | +| $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{I}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | +| $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\mathcal{I}`$ — which way the row binds — `<=`, `>=` or `==` | +| $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, a non-extendable build — is folded in here by data prep | | $`\mathrm{last}`$ | `snapshot_is_last` over $`\mathcal{T}`$ — one at the horizon's last snapshot, zero elsewhere — data prep, how an expression reads a final level | -| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\mathcal{B} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency, data prep; a generator of an unweighted carrier has no row | -| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\mathcal{B} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | -| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\mathcal{B} \times \mathcal{V}`$ — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | -| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\mathcal{B} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row | -| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\mathcal{B} \times \mathcal{S}`$ — one where the storage unit is in the row's set — data prep; one outside it has no row | -| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\mathcal{B} \times \mathcal{V}`$ — one where the store is in the row's set — data prep; one outside it has no row | -| $`\mathrm{len}`$ | `Line_volume_weight` over $`\mathcal{B} \times \mathcal{K}`$ — the line's length where its carrier is in the row's set — data prep; a line outside it has no row | -| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\mathcal{B} \times \mathcal{L}`$ — the link's length where its carrier is in the row's set — data prep; a link outside it has no row | -| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\mathcal{B} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set — data prep; a line outside it has no row | -| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\mathcal{B} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set — data prep; a link outside it has no row | -| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{L}`$ — one where the link is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{S}`$ — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{V}`$ — one where the store is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{B} \times \mathcal{J}`$ — one where the process is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\mathcal{I} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency, data prep; a generator of an unweighted carrier has no row | +| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\mathcal{I} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | +| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\mathcal{I} \times \mathcal{V}`$ — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | +| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row | +| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's set — data prep; one outside it has no row | +| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's set — data prep; one outside it has no row | +| $`\mathrm{len}`$ | `Line_volume_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's length where its carrier is in the row's set — data prep; a line outside it has no row | +| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's length where its carrier is in the row's set — data prep; a link outside it has no row | +| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set — data prep; a line outside it has no row | +| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set — data prep; a link outside it has no row | +| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{L}`$ — one where the link is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{J}`$ — one where the process is in the row's carrier-and-bus set — data prep; one outside it has no row | #### Variables @@ -1801,6 +1870,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{up}`$ | `Generator_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | | $`\mathit{dn}`$ | `Generator_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | +| $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the tangents; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | @@ -1828,11 +1898,11 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | | $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | | $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed transfer wraps from the horizon's end, and where it is not the energy still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | -| $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{B}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | -| $`\mathit{operational\_limit}`$ | `operational_limit` over $`\Xi \times \mathcal{B}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | -| $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\mathcal{B}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | -| $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\mathcal{B}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | -| $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{B}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | +| $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{I}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | +| $`\mathit{operational\_limit}`$ | `operational_limit` over $`\Xi \times \mathcal{I}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | +| $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\mathcal{I}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | +| $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\mathcal{I}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | +| $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | $`t \ominus k`$ denotes cyclic translation: index $`t-k`$ taken modulo the size of the dimension (`roll`). Plain $`t-k`$ (`shift`) has no wraparound — terms translated past the edge are simply absent. @@ -2779,14 +2849,14 @@ Generator_shut_down_p_nom_variable_upper: ```yaml Line_fix_s_lower: - description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating" + description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating, the loss counted against it" dims: [scenario, snapshot, line] where: not Line_s_nom_extendable AND Line_active - expression: Line_s >= -Line_s_max_pu * Line_s_nom + expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom ``` ```math -s_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} - \ell_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-fix-s-upper` @@ -2795,14 +2865,14 @@ s_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} ```yaml Line_fix_s_upper: - description: "`Line-fix-s-upper` — a fixed line carries at most its rating" + description: "`Line-fix-s-upper` — a fixed line carries at most its rating, the loss included" dims: [scenario, snapshot, line] where: not Line_s_nom_extendable AND Line_active - expression: Line_s <= Line_s_max_pu * Line_s_nom + expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom ``` ```math -s_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} + \ell_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s-lower` @@ -2811,14 +2881,14 @@ s_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} ```yaml Line_ext_s_lower: - description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build" + description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build, the loss counted against it" dims: [scenario, snapshot, line] where: Line_s_nom_extendable AND Line_active - expression: Line_s >= -Line_s_max_pu * Line_s_nom_ext + expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom_ext ``` ```math -s_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} - \ell_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s-upper` @@ -2827,14 +2897,14 @@ s_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \i ```yaml Line_ext_s_upper: - description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build" + description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build, the loss included" dims: [scenario, snapshot, line] where: Line_s_nom_extendable AND Line_active - expression: Line_s <= Line_s_max_pu * Line_s_nom_ext + expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom_ext ``` ```math -s_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} + \ell_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s_nom-lower` @@ -2901,6 +2971,57 @@ Line_s_set: s_{\xi,t,k} = \mathrm{s}^{\mathrm{set}}_{t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{s}^{\mathrm{set}}_{t,k} \text{ is defined} \wedge \mathrm{on}^{s}_{t,k} ``` +### `Line-loss_upper` + +`Line_loss_upper` + +```yaml +Line_loss_upper: + description: "`Line-loss_upper` — a line dissipates at most the loss at its rating" + dims: [scenario, snapshot, line] + where: transmission_losses AND Line_active + expression: Line_loss <= Line_loss_max +``` + +```math +\ell_{\xi,t,k} \le \overline{\ell}_{t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} +``` + +### `Line-loss_tangents-{k}-1` + +`Line_loss_tangents_forward` + +```yaml +Line_loss_tangents_forward: + description: >- + `Line-loss_tangents-{k}-1` — the loss sits above every tangent to its + curve for flow one way; PyPSA names one row per segment `k`, this block + states them all over the segment dimension + dims: [scenario, snapshot, line, segment] + where: transmission_losses AND Line_active + expression: Line_loss + Line_loss_slope * Line_s >= Line_loss_offset +``` + +```math +\ell_{\xi,t,k} + \mathrm{a}_{t,k,b} \cdot s_{\xi,t,k} \ge \mathrm{b}_{t,k,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} +``` + +### `Line-loss_tangents-{k}--1` + +`Line_loss_tangents_reverse` + +```yaml +Line_loss_tangents_reverse: + description: "`Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending on the flow's magnitude" + dims: [scenario, snapshot, line, segment] + where: transmission_losses AND Line_active + expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset +``` + +```math +\ell_{\xi,t,k} - \mathrm{a}_{t,k,b} \cdot s_{\xi,t,k} \ge \mathrm{b}_{t,k,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} +``` + ### `Transformer-fix-s-lower` `Transformer_fix_s_lower` @@ -3580,7 +3701,7 @@ GlobalConstraint_primary_energy_ub: ``` ```math -\mathit{primary\_energy}_{\xi,b} \le \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} +\mathit{primary\_energy}_{\xi,i} \le \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} ``` ### `primary_energy` @@ -3596,7 +3717,7 @@ GlobalConstraint_primary_energy_lb: ``` ```math -\mathit{primary\_energy}_{\xi,b} \ge \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} +\mathit{primary\_energy}_{\xi,i} \ge \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} ``` ### `primary_energy` @@ -3612,7 +3733,7 @@ GlobalConstraint_primary_energy_eq: ``` ```math -\mathit{primary\_energy}_{\xi,b} = \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} +\mathit{primary\_energy}_{\xi,i} = \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} ``` ### `operational_limit` @@ -3628,7 +3749,7 @@ GlobalConstraint_operational_limit_ub: ``` ```math -\mathit{operational\_limit}_{\xi,b} \le \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} +\mathit{operational\_limit}_{\xi,i} \le \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} ``` ### `operational_limit` @@ -3644,7 +3765,7 @@ GlobalConstraint_operational_limit_lb: ``` ```math -\mathit{operational\_limit}_{\xi,b} \ge \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} +\mathit{operational\_limit}_{\xi,i} \ge \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} ``` ### `operational_limit` @@ -3660,7 +3781,7 @@ GlobalConstraint_operational_limit_eq: ``` ```math -\mathit{operational\_limit}_{\xi,b} = \mathrm{K}_{b} \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} +\mathit{operational\_limit}_{\xi,i} = \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -3676,7 +3797,7 @@ GlobalConstraint_transmission_volume_expansion_limit_ub: ``` ```math -\mathit{transmission\_volume\_expansion}_{b} \le \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} +\mathit{transmission\_volume\_expansion}_{i} \le \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -3692,7 +3813,7 @@ GlobalConstraint_transmission_volume_expansion_limit_lb: ``` ```math -\mathit{transmission\_volume\_expansion}_{b} \ge \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} +\mathit{transmission\_volume\_expansion}_{i} \ge \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -3708,7 +3829,7 @@ GlobalConstraint_transmission_volume_expansion_limit_eq: ``` ```math -\mathit{transmission\_volume\_expansion}_{b} = \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} +\mathit{transmission\_volume\_expansion}_{i} = \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -3724,7 +3845,7 @@ GlobalConstraint_transmission_expansion_cost_limit_ub: ``` ```math -\mathit{transmission\_expansion\_cost}_{b} \le \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} +\mathit{transmission\_expansion\_cost}_{i} \le \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -3740,7 +3861,7 @@ GlobalConstraint_transmission_expansion_cost_limit_lb: ``` ```math -\mathit{transmission\_expansion\_cost}_{b} \ge \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} +\mathit{transmission\_expansion\_cost}_{i} \ge \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -3756,7 +3877,7 @@ GlobalConstraint_transmission_expansion_cost_limit_eq: ``` ```math -\mathit{transmission\_expansion\_cost}_{b} = \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} +\mathit{transmission\_expansion\_cost}_{i} = \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} ``` ### `tech_capacity_expansion_limit` @@ -3772,7 +3893,7 @@ GlobalConstraint_tech_capacity_expansion_limit_ub: ``` ```math -\mathit{tech\_capacity\_expansion}_{b} \le \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{<=}\text{'} +\mathit{tech\_capacity\_expansion}_{i} \le \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} ``` ### `tech_capacity_expansion_limit` @@ -3788,7 +3909,7 @@ GlobalConstraint_tech_capacity_expansion_limit_lb: ``` ```math -\mathit{tech\_capacity\_expansion}_{b} \ge \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{>=}\text{'} +\mathit{tech\_capacity\_expansion}_{i} \ge \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} ``` ### `tech_capacity_expansion_limit` @@ -3804,7 +3925,7 @@ GlobalConstraint_tech_capacity_expansion_limit_eq: ``` ```math -\mathit{tech\_capacity\_expansion}_{b} = \mathrm{K}_{b} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{type}_{b} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{b} = \text{'}\mathrm{==}\text{'} +\mathit{tech\_capacity\_expansion}_{i} = \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} ``` ### `Bus-nodal_balance` @@ -3818,7 +3939,8 @@ Bus_nodal_balance: stores included, less what the links take away, plus what arrives over them after losses and any delay at every port they deliver to, each process port drawing or delivering at its own rate and each passive branch - carrying its flow, meets the load there. + carrying its flow, meets the load there, less half of every incident + line's loss — PyPSA dissipates a branch's loss half at either end. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] @@ -3831,13 +3953,15 @@ Bus_nodal_balance: + sum(Process_output_arrival, by=Process_output_bus, over=process_output, into=bus) - sum(Line_s, by=Line_bus0, over=line, into=bus) + sum(Line_s, by=Line_bus1, over=line, into=bus) + - 0.5 * sum(Line_loss, by=Line_bus0, over=line, into=bus) + - 0.5 * sum(Line_loss, by=Line_bus1, over=line, into=bus) - sum(Transformer_s, by=Transformer_bus0, over=transformer, into=bus) + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) == sum(Load_p_set, by=Load_bus, over=load, into=bus) ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} ``` ### `Carrier-growth_limit` @@ -4127,7 +4251,7 @@ primary_energy: ``` ```math -\mathit{primary\_energy}_{\xi,b} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{a}_{b,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{h}_{b,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{e}_{b,v} \right) \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} +\mathit{primary\_energy}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{a}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `operational_limit` @@ -4145,7 +4269,7 @@ operational_limit: ``` ```math -\mathit{operational\_limit}_{\xi,b} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{b}_{b,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{h}_{b,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{e}_{b,v} \right) \qquad \forall\, \xi \in \Xi,\ b \in \mathcal{B} +\mathit{operational\_limit}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{b}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `transmission_volume_expansion` @@ -4159,7 +4283,7 @@ transmission_volume_expansion: ``` ```math -\mathit{transmission\_volume\_expansion}_{b} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{len}_{b,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{len}^{f}_{b,l} \qquad \forall\, b \in \mathcal{B} +\mathit{transmission\_volume\_expansion}_{i} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{len}_{i,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{len}^{f}_{i,l} \qquad \forall\, i \in \mathcal{I} ``` ### `transmission_expansion_cost` @@ -4173,7 +4297,7 @@ transmission_expansion_cost: ``` ```math -\mathit{transmission\_expansion\_cost}_{b} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{cc}_{b,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{cc}^{f}_{b,l} \qquad \forall\, b \in \mathcal{B} +\mathit{transmission\_expansion\_cost}_{i} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{cc}_{i,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{cc}^{f}_{i,l} \qquad \forall\, i \in \mathcal{I} ``` ### `tech_capacity_expansion` @@ -4191,7 +4315,7 @@ tech_capacity_expansion: ``` ```math -\mathit{tech\_capacity\_expansion}_{b} = \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{m}_{b,g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{m}^{f}_{b,l} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{m}^{l}_{b,k} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{m}^{h}_{b,s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{m}^{e}_{b,v} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{m}^{z}_{b,j} \qquad \forall\, b \in \mathcal{B} +\mathit{tech\_capacity\_expansion}_{i} = \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{m}_{i,g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{m}^{f}_{i,l} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{m}^{l}_{i,k} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{m}^{h}_{i,s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{m}^{e}_{i,v} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{m}^{z}_{i,j} \qquad \forall\, i \in \mathcal{I} ``` ### `scenario_opex` @@ -4305,6 +4429,12 @@ u_{\xi,t,g} \ge 0, u_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \ s_{\xi,t,k} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{on}^{s}_{t,k} ``` +**`Line_loss`** + +```math +\ell_{\xi,t,k} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} +``` + **`Transformer_s`** ```math diff --git a/docs/examples/pypsa_losses.md b/docs/examples/pypsa_losses.md deleted file mode 100644 index 441da45b..00000000 --- a/docs/examples/pypsa_losses.md +++ /dev/null @@ -1,426 +0,0 @@ - - -# PyPSA, the lossy lines - -Rung 13 of [PyPSA in one file](pypsa.md): `n.optimize(transmission_losses={'mode': 'tangents', 'segments': K})`, stated on rung 6's lines in a -file of its own. Its network is the spine plus the script's own additions. - -## Rung 13 — transmission losses - -| PyPSA | status | note | -| --- | --- | --- | -| [`Line-loss`](#variable-domains) | done | | -| [`Line-fix-s-*`, `Line-ext-s-*`](#line-fix-s-lower) | done | the loss counted against the rating | -| [`Bus-nodal_balance`](#bus-nodal_balance) | done | half of each incident line's loss at either end | -| [`Line-loss_upper`](#line-loss_upper) | done | `loss_max` is data prep | -| [`Line-loss_tangents-{k}-1`](#line-loss_tangents-k-1) | split | PyPSA names a row per segment; one block over the dimension | -| [`Line-loss_tangents-{k}--1`](#line-loss_tangents-k--1) | split | | -| `Line-loss_secants-*` | out | the secant mode solves for its segment count | - - -> ✔ `pypsa 1.3.0` solves this rung's network at objective `10645.295879552297`, 150 rows. - -
-The network, as PyPSA code - -`rung_13_losses.py` - -```python -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -"""Rung 13: transmission losses in tangent form — a loss per line, stated by `pypsa_losses.yaml`.""" - -from __future__ import annotations - -import spine - -MODEL = 'pypsa_losses.yaml' -OPTIMIZE = {'transmission_losses': {'mode': 'tangents', 'segments': 2}} - - -def build(): - """The spine plus a 110 kV triangle of lines, one of them extendable — ohms a real line has, so the loss stays a few percent of the flow.""" - n = spine.build() - n.add('Bus', ['a', 'b', 'c'], v_nom=110) - n.add('Generator', 'hydro13', bus='a', p_nom=80, marginal_cost=10) - n.add('Generator', 'diesel13', bus='b', p_nom=80, marginal_cost=50) - n.add('Line', 'ab13', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) - n.add('Line', 'bc13', bus0='b', bus1='c', carrier='AC', x=60, r=9.7, s_nom=60) - n.add( - 'Line', - 'ca13', - bus0='c', - bus1='a', - carrier='AC', - x=45, - r=6, - s_nom=40, - s_nom_extendable=True, - s_nom_max=90, - capital_cost=4, - ) - n.add('Load', 'town13', bus='c', p_set=[35, 55, 15, 45]) - return n -``` - -
- - -## The file - - -The lossy class of a plain `n.optimize()`: `transmission_losses` in its tangent form, stated on rung 6's lines in a file of its own. A line dissipates a loss its flow buys along a fan of tangents to the quadratic curve, half at either end — a variable and rows the keyword adds, which no `where:` can add to `examples/pypsa.yaml`. The fan's slopes and offsets are data prep, one per segment. - -#### Sets - -| Symbol | Meaning | -|---|---| -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | -| $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | -| $`\mathcal{K}`$ | index $`k`$ — `segment` — the tangents the loss curve is approximated by, PyPSA's `segments` | -| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | - -#### Parameters - -| Symbol | Meaning | -|---|---| -| $`\mathrm{w}`$ | `snapshot_weightings_objective` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost | -| $`\mathrm{p}^{\mathrm{nom}}`$ | `Generator_p_nom` over $`\mathcal{G}`$ — nominal power | -| $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | -| $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | -| $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | -| $`\mathrm{s}^{\mathrm{nom}}`$ | `Line_s_nom` over $`\mathcal{K}`$ — nominal apparent power | -| $`\mathrm{ext}^{s}`$ | `Line_s_nom_extendable` over $`\mathcal{K}`$ — whether the nominal apparent power is a decision | -| $`\overline{\mathrm{s}}`$ | `Line_s_max_pu` over $`\mathcal{T} \times \mathcal{K}`$ — most flow either way, per unit of nominal apparent power | -| $`\underline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_min` over $`\mathcal{K}`$ — least nominal apparent power an extendable line may be built at | -| $`\overline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_max` over $`\mathcal{K}`$ — most nominal apparent power an extendable line may be built at | -| $`\mathrm{c}^{\mathrm{cap},s}`$ | `Line_capital_cost` over $`\mathcal{K}`$ — cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row | -| $`\overline{\ell}`$ | `Line_loss_max` over $`\mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | -| $`\mathrm{a}`$ | `Line_loss_slope` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{K}`$ — the slope of a tangent to the loss curve at its segment's flow — `2 * r_pu_eff * p_k`, data prep | -| $`\mathrm{b}`$ | `Line_loss_offset` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{K}`$ — where that tangent meets the loss axis — `loss_k - slope_k * p_k`, negative, data prep | -| $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\mathcal{L}`$ — nominal power | -| $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | -| $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | -| $`\eta`$ | `Link_efficiency` over $`\mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | -| $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | -| $`\mathrm{load}`$ | `Load_p_set` over $`\mathcal{T} \times \mathcal{D}`$ — demand | - -#### Variables - -| Symbol | Meaning | -|---|---| -| $`p`$ | `Generator_p` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-p` — output of a generator in a snapshot | -| $`s`$ | `Line_s` over $`\mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1` | -| $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | -| $`f`$ | `Link_p` over $`\mathcal{T} \times \mathcal{L}`$ — `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a positive value withdraws there and injects at every bus the link's output ports deliver to | -| $`\ell`$ | `Line_loss` over $`\mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the tangents | - -### Objective - -```yaml -objective: - sense: minimize - description: operating cost by weighted snapshot, plus what the lines cost to build - expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective) - + sum(Line_s_nom_ext * Line_capital_cost) -``` - -```math -\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} + \sum_{t \in \mathcal{T},\ l \in \mathcal{L}} f_{t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} -``` - -### `Generator-fix-p-lower` - -`Generator_fix_p_lower` - -```yaml -Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [snapshot, generator] - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom -``` - -```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} -``` - -### `Generator-fix-p-upper` - -`Generator_fix_p_upper` - -```yaml -Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [snapshot, generator] - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom -``` - -```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} -``` - -### `Link-fix-p-lower` - -`Link_fix_p_lower` - -```yaml -Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom -``` - -```math -f_{t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Link-fix-p-upper` - -`Link_fix_p_upper` - -```yaml -Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom -``` - -```math -f_{t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -### `Line-fix-s-lower` - -`Line_fix_s_lower` - -```yaml -Line_fix_s_lower: - description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating, the loss counted against it" - dims: [snapshot, line] - where: not Line_s_nom_extendable - expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom -``` - -```math -s_{t,k} - \ell_{t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} -``` - -### `Line-fix-s-upper` - -`Line_fix_s_upper` - -```yaml -Line_fix_s_upper: - description: "`Line-fix-s-upper` — a fixed line carries at most its rating, loss included" - dims: [snapshot, line] - where: not Line_s_nom_extendable - expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom -``` - -```math -s_{t,k} + \ell_{t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} -``` - -### `Line-ext-s-lower` - -`Line_ext_s_lower` - -```yaml -Line_ext_s_lower: - description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable - expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom_ext -``` - -```math -s_{t,k} - \ell_{t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} -``` - -### `Line-ext-s-upper` - -`Line_ext_s_upper` - -```yaml -Line_ext_s_upper: - description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable - expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom_ext -``` - -```math -s_{t,k} + \ell_{t,k} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} -``` - -### `Line-ext-s_nom-lower` - -`Line_ext_s_nom_lower` - -```yaml -Line_ext_s_nom_lower: - description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor" - dims: [line] - where: Line_s_nom_extendable - expression: Line_s_nom_ext >= Line_s_nom_min -``` - -```math -S_{k} \ge \underline{\mathrm{s}}^{\mathrm{nom}}_{k} \qquad \forall\, k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} -``` - -### `Line-ext-s_nom-upper` - -`Line_ext_s_nom_upper` - -```yaml -Line_ext_s_nom_upper: - description: "`Line-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [line] - where: Line_s_nom_extendable AND Line_s_nom_max - expression: Line_s_nom_ext <= Line_s_nom_max -``` - -```math -S_{k} \le \overline{\mathrm{s}}^{\mathrm{nom}}_{k} \qquad \forall\, k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \overline{\mathrm{s}}^{\mathrm{nom}}_{k} \text{ is defined} -``` - -### `Kirchhoff-Voltage-Law` - -`Kirchhoff_Voltage_Law` - -```yaml -Kirchhoff_Voltage_Law: - description: >- - `Kirchhoff-Voltage-Law` — around every independent cycle the - impedance-weighted flows sum to nothing, which is what makes the linear - power flow physical rather than transport - dims: [snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 -``` - -```math -\sum_{k \in \mathcal{K}} s_{t,k} \cdot \mathrm{x}_{k,c} = 0 \qquad \forall\, t \in \mathcal{T},\ c \in \mathcal{C} -``` - -### `Bus-nodal_balance` - -`Bus_nodal_balance` - -```yaml -Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, plus what the links and - lines bring, meets the load there, less half of every incident line's - loss — PyPSA dissipates a branch's loss half at either end - dims: [snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - - sum(Line_s, by=Line_bus0, over=line, into=bus) - + sum(Line_s, by=Line_bus1, over=line, into=bus) - - 0.5 * sum(Line_loss, by=Line_bus0, over=line, into=bus) - - 0.5 * sum(Line_loss, by=Line_bus1, over=line, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) -``` - -```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{t,g} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} f_{t,\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{t,k} \right) = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} -``` - -### `Line-loss_upper` - -`Line_loss_upper` - -```yaml -Line_loss_upper: - description: "`Line-loss_upper` — a line dissipates at most the loss at its rating" - dims: [snapshot, line] - expression: Line_loss <= Line_loss_max -``` - -```math -\ell_{t,k} \le \overline{\ell}_{t,k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} -``` - -### `Line-loss_tangents-{k}-1` - -`Line_loss_tangents_forward` - -```yaml -Line_loss_tangents_forward: - description: >- - `Line-loss_tangents-{k}-1` — the loss sits above every tangent to its - curve for flow one way; PyPSA names one row per segment `k`, this block - states them all over the segment dimension - dims: [snapshot, line, segment] - expression: Line_loss + Line_loss_slope * Line_s >= Line_loss_offset -``` - -```math -\ell_{t,k} + \mathrm{a}_{t,k,k} \cdot s_{t,k} \ge \mathrm{b}_{t,k,k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K},\ k \in \mathcal{K} -``` - -### `Line-loss_tangents-{k}--1` - -`Line_loss_tangents_reverse` - -```yaml -Line_loss_tangents_reverse: - description: "`Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending on the flow's magnitude" - dims: [snapshot, line, segment] - expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset -``` - -```math -\ell_{t,k} - \mathrm{a}_{t,k,k} \cdot s_{t,k} \ge \mathrm{b}_{t,k,k} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K},\ k \in \mathcal{K} -``` - -#### Variable domains - -**`Generator_p`** - -```math -p_{t,g} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} -``` - -**`Line_s`** - -```math -s_{t,k} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} -``` - -**`Line_s_nom_ext`** - -```math -S_{k} \in \mathbb{R} \qquad \forall\, k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} -``` - -**`Link_p`** - -```math -f_{t,l} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} -``` - -**`Line_loss`** - -```math -\ell_{t,k} \ge 0 \qquad \forall\, t \in \mathcal{T},\ k \in \mathcal{K} -``` - - -Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 591b4fc6..e85e1e05 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -49,6 +49,9 @@ dimensions: description: passive branches between two buses, their flow set by impedance and tap ratio, with a fixed phase shift cycle: description: independent cycles of the passive network graph — the cycle basis, data prep + segment: + description: the tangents a line's loss curve is approximated by — PyPSA's `transmission_losses` count; none in a lossless run + dtype: int global_constraint: description: PyPSA's `GlobalConstraint` rows, one label per declared limit period: @@ -498,6 +501,22 @@ parameters: the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row dims: [line, cycle] + transmission_losses: + description: >- + whether the network dissipates transmission losses — PyPSA's + `transmission_losses` count read as a flag, its tangents the `segment` + axis; false with no segments is a lossless run + dims: [] + dtype: bool + Line_loss_max: + description: the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep + dims: [snapshot, line] + Line_loss_slope: + description: the slope of a tangent to the loss curve at its segment's flow — `2 * r_pu_eff * p_k`, data prep + dims: [snapshot, line, segment] + Line_loss_offset: + description: where that tangent meets the loss axis — `loss_k - slope_k * p_k`, negative, data prep + dims: [snapshot, line, segment] Transformer_s_nom: description: nominal apparent power dims: [transformer] @@ -767,6 +786,16 @@ variables: positive value withdraws there and injects at `Line_bus1`, lossless dims: [scenario, snapshot, line] where: Line_active + Line_loss: + description: >- + `Line-loss` — what a line dissipates carrying its flow, pushed down by the + cost and held up by the tangents; absent, and zero in the balance, where + the network is lossless + dims: [scenario, snapshot, line] + where: transmission_losses AND Line_active + absence: zero + bounds: + lower: 0 Transformer_s: description: >- `Transformer-s` — PyPSA's `p0`, the flow measured at the @@ -1332,25 +1361,25 @@ constraints: where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_shut_down <= Generator_n_mod Line_fix_s_lower: - description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating" + description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating, the loss counted against it" dims: [scenario, snapshot, line] where: not Line_s_nom_extendable AND Line_active - expression: Line_s >= -Line_s_max_pu * Line_s_nom + expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom Line_fix_s_upper: - description: "`Line-fix-s-upper` — a fixed line carries at most its rating" + description: "`Line-fix-s-upper` — a fixed line carries at most its rating, the loss included" dims: [scenario, snapshot, line] where: not Line_s_nom_extendable AND Line_active - expression: Line_s <= Line_s_max_pu * Line_s_nom + expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom Line_ext_s_lower: - description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build" + description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build, the loss counted against it" dims: [scenario, snapshot, line] where: Line_s_nom_extendable AND Line_active - expression: Line_s >= -Line_s_max_pu * Line_s_nom_ext + expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom_ext Line_ext_s_upper: - description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build" + description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build, the loss included" dims: [scenario, snapshot, line] where: Line_s_nom_extendable AND Line_active - expression: Line_s <= Line_s_max_pu * Line_s_nom_ext + expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom_ext Line_ext_s_nom_lower: description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor" dims: [line] @@ -1371,6 +1400,24 @@ constraints: dims: [scenario, snapshot, line] where: Line_s_set AND Line_active expression: Line_s == Line_s_set + Line_loss_upper: + description: "`Line-loss_upper` — a line dissipates at most the loss at its rating" + dims: [scenario, snapshot, line] + where: transmission_losses AND Line_active + expression: Line_loss <= Line_loss_max + Line_loss_tangents_forward: + description: >- + `Line-loss_tangents-{k}-1` — the loss sits above every tangent to its + curve for flow one way; PyPSA names one row per segment `k`, this block + states them all over the segment dimension + dims: [scenario, snapshot, line, segment] + where: transmission_losses AND Line_active + expression: Line_loss + Line_loss_slope * Line_s >= Line_loss_offset + Line_loss_tangents_reverse: + description: "`Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending on the flow's magnitude" + dims: [scenario, snapshot, line, segment] + where: transmission_losses AND Line_active + expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset Transformer_fix_s_lower: description: "`Transformer-fix-s-lower` — a fixed transformer carries at least the negative of its rating" dims: [scenario, snapshot, transformer] @@ -1689,7 +1736,8 @@ constraints: stores included, less what the links take away, plus what arrives over them after losses and any delay at every port they deliver to, each process port drawing or delivering at its own rate and each passive branch - carrying its flow, meets the load there. + carrying its flow, meets the load there, less half of every incident + line's loss — PyPSA dissipates a branch's loss half at either end. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] @@ -1702,6 +1750,8 @@ constraints: + sum(Process_output_arrival, by=Process_output_bus, over=process_output, into=bus) - sum(Line_s, by=Line_bus0, over=line, into=bus) + sum(Line_s, by=Line_bus1, over=line, into=bus) + - 0.5 * sum(Line_loss, by=Line_bus0, over=line, into=bus) + - 0.5 * sum(Line_loss, by=Line_bus1, over=line, into=bus) - sum(Transformer_s, by=Transformer_bus0, over=transformer, into=bus) + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) == sum(Load_p_set, by=Load_bus, over=load, into=bus) diff --git a/examples/pypsa_losses.yaml b/examples/pypsa_losses.yaml deleted file mode 100644 index 5ffb4d9c..00000000 --- a/examples/pypsa_losses.yaml +++ /dev/null @@ -1,260 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -description: >- - The lossy class of a plain `n.optimize()`: `transmission_losses` in its - tangent form, stated on rung 6's lines in a file of its own. A line - dissipates a loss its flow buys along a fan of tangents to the quadratic - curve, half at either end — a variable and rows the keyword adds, which no - `where:` can add to `examples/pypsa.yaml`. The fan's slopes and offsets are - data prep, one per segment. - -dimensions: - snapshot: - description: dispatch periods - dtype: datetime - bus: - description: network nodes - generator: - description: generating units, each on one bus - link: - description: controllable connections, each from one bus to the buses it delivers to - link_output: - description: >- - a link's output ports, one label per port a link declares — PyPSA's - `bus1`, `bus2`, … columns read long, so a link of any number of output - ports is one term in the balance, data prep - line: - description: passive branches, each between two buses, their flow set by impedance - cycle: - description: independent cycles of the passive network graph — the cycle basis, data prep - segment: - description: the tangents the loss curve is approximated by, PyPSA's `segments` - dtype: int - load: - description: demands, each on one bus - -parameters: - snapshot_weightings_objective: - description: PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost - dims: [snapshot] - Generator_p_nom: - description: nominal power - dims: [generator] - Generator_p_min_pu: - description: least output, per unit of nominal power - dims: [snapshot, generator] - Generator_p_max_pu: - description: most output, per unit of nominal power — an availability profile - dims: [snapshot, generator] - Generator_marginal_cost: - description: cost of one unit of output - dims: [snapshot, generator] - Line_s_nom: - description: nominal apparent power - dims: [line] - Line_s_nom_extendable: - description: whether the nominal apparent power is a decision - dims: [line] - dtype: bool - Line_s_max_pu: - description: most flow either way, per unit of nominal apparent power - dims: [snapshot, line] - Line_s_nom_min: - description: least nominal apparent power an extendable line may be built at - dims: [line] - Line_s_nom_max: - description: most nominal apparent power an extendable line may be built at - dims: [line] - Line_capital_cost: - description: cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [line] - Line_cycle_weight: - description: >- - the line's series impedance, signed by its orientation in the cycle — - the cycle basis, data prep; a line in no cycle has no row - dims: [line, cycle] - Line_loss_max: - description: the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep - dims: [snapshot, line] - Line_loss_slope: - description: the slope of a tangent to the loss curve at its segment's flow — `2 * r_pu_eff * p_k`, data prep - dims: [snapshot, line, segment] - Line_loss_offset: - description: where that tangent meets the loss axis — `loss_k - slope_k * p_k`, negative, data prep - dims: [snapshot, line, segment] - Link_p_nom: - description: nominal power - dims: [link] - Link_p_min_pu: - description: least flow, per unit of nominal power — negative for a link that carries both ways - dims: [snapshot, link] - Link_p_max_pu: - description: most flow, per unit of nominal power - dims: [snapshot, link] - Link_efficiency: - description: >- - share of the flow that arrives at an output port, PyPSA's `efficiency`, - `efficiency2`, … read long — negative where that port consumes rather - than delivers - dims: [link_output] - Link_marginal_cost: - description: cost of one unit of flow - dims: [snapshot, link] - Load_p_set: - description: demand - dims: [snapshot, load] - -relations: - Generator_bus: - description: the bus a generator sits on - key: generator - values: bus - Line_bus0: - description: the bus a line's flow is measured at - key: line - values: bus - Line_bus1: - description: the bus at a line's other end - key: line - values: bus - Link_bus0: - description: the bus a link leaves - key: link - values: bus - Link_output_link: - description: the link an output port belongs to - key: link_output - values: link - Link_output_bus: - description: >- - the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. - A link of three output ports is three labels here rather than a third - relation, so the file states any number of them - key: link_output - values: bus - Load_bus: - description: the bus a load sits on - key: load - values: bus - -variables: - Generator_p: - description: "`Generator-p` — output of a generator in a snapshot" - dims: [snapshot, generator] - Line_s: - description: >- - `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a - positive value withdraws there and injects at `Line_bus1` - dims: [snapshot, line] - Line_s_nom_ext: - description: >- - `Line-s_nom` — nominal apparent power where it is a decision; the - parameter of the same PyPSA name carries the fixed regime - dims: [line] - where: Line_s_nom_extendable - Link_p: - description: >- - `Link-p` — PyPSA's `p0`, the flow measured at the `Link_bus0` end: a - positive value withdraws there and injects at every bus the link's - output ports deliver to - dims: [snapshot, link] - Line_loss: - description: "`Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the tangents" - dims: [snapshot, line] - bounds: - lower: 0 - -constraints: - Generator_fix_p_lower: - description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" - dims: [snapshot, generator] - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom - Generator_fix_p_upper: - description: "`Generator-fix-p-upper` — a generator outputs at most what is available" - dims: [snapshot, generator] - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom - Link_fix_p_lower: - description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" - dims: [snapshot, link] - expression: Link_p >= Link_p_min_pu * Link_p_nom - Link_fix_p_upper: - description: "`Link-fix-p-upper` — a link carries at most its nominal power" - dims: [snapshot, link] - expression: Link_p <= Link_p_max_pu * Link_p_nom - Line_fix_s_lower: - description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating, the loss counted against it" - dims: [snapshot, line] - where: not Line_s_nom_extendable - expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom - Line_fix_s_upper: - description: "`Line-fix-s-upper` — a fixed line carries at most its rating, loss included" - dims: [snapshot, line] - where: not Line_s_nom_extendable - expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom - Line_ext_s_lower: - description: "`Line-ext-s-lower` — an extendable line carries at least the negative of its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable - expression: Line_s - Line_loss >= -Line_s_max_pu * Line_s_nom_ext - Line_ext_s_upper: - description: "`Line-ext-s-upper` — an extendable line carries at most its rating of the chosen build" - dims: [snapshot, line] - where: Line_s_nom_extendable - expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom_ext - Line_ext_s_nom_lower: - description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor" - dims: [line] - where: Line_s_nom_extendable - expression: Line_s_nom_ext >= Line_s_nom_min - Line_ext_s_nom_upper: - description: "`Line-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [line] - where: Line_s_nom_extendable AND Line_s_nom_max - expression: Line_s_nom_ext <= Line_s_nom_max - Kirchhoff_Voltage_Law: - description: >- - `Kirchhoff-Voltage-Law` — around every independent cycle the - impedance-weighted flows sum to nothing, which is what makes the linear - power flow physical rather than transport - dims: [snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 - Bus_nodal_balance: - description: >- - `Bus-nodal_balance` — what is generated at a bus, plus what the links and - lines bring, meets the load there, less half of every incident line's - loss — PyPSA dissipates a branch's loss half at either end - dims: [snapshot, bus] - expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - - sum(Link_p, by=Link_bus0, over=link, into=bus) - + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - - sum(Line_s, by=Line_bus0, over=line, into=bus) - + sum(Line_s, by=Line_bus1, over=line, into=bus) - - 0.5 * sum(Line_loss, by=Line_bus0, over=line, into=bus) - - 0.5 * sum(Line_loss, by=Line_bus1, over=line, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) - Line_loss_upper: - description: "`Line-loss_upper` — a line dissipates at most the loss at its rating" - dims: [snapshot, line] - expression: Line_loss <= Line_loss_max - Line_loss_tangents_forward: - description: >- - `Line-loss_tangents-{k}-1` — the loss sits above every tangent to its - curve for flow one way; PyPSA names one row per segment `k`, this block - states them all over the segment dimension - dims: [snapshot, line, segment] - expression: Line_loss + Line_loss_slope * Line_s >= Line_loss_offset - Line_loss_tangents_reverse: - description: "`Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending on the flow's magnitude" - dims: [snapshot, line, segment] - expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset - -objective: - sense: minimize - description: operating cost by weighted snapshot, plus what the lines cost to build - expression: >- - sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective) - + sum(Link_p * Link_marginal_cost * snapshot_weightings_objective) - + sum(Line_s_nom_ext * Line_capital_cost) diff --git a/examples/references/pypsa/rung_13_losses.py b/examples/references/pypsa/rung_13_losses.py index 182ac34b..424af196 100644 --- a/examples/references/pypsa/rung_13_losses.py +++ b/examples/references/pypsa/rung_13_losses.py @@ -2,13 +2,12 @@ # # SPDX-License-Identifier: MIT -"""Rung 13: transmission losses in tangent form — a loss per line, stated by `pypsa_losses.yaml`.""" +"""Rung 13: transmission losses in tangent form — a loss per line.""" from __future__ import annotations import spine -MODEL = 'pypsa_losses.yaml' OPTIMIZE = {'transmission_losses': {'mode': 'tangents', 'segments': 2}} diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 269b1c21..6f1b60f3 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -127,6 +127,11 @@ names: Line_s_nom_set: '\mathrm{s}^{\mathrm{nom,set}}' Line_s_set: '\mathrm{s}^{\mathrm{set}}' Line_cycle_weight: '\mathrm{x}' + transmission_losses: '\mathrm{lossy}' + Line_loss: '\ell' + Line_loss_max: '\overline{\ell}' + Line_loss_slope: '\mathrm{a}' + Line_loss_offset: '\mathrm{b}' Transformer_s: '\sigma' Transformer_s_nom_ext: '\Sigma' Transformer_s_nom: '\sigma^{\mathrm{nom}}' diff --git a/examples/symbols/pypsa_losses.yaml b/examples/symbols/pypsa_losses.yaml deleted file mode 100644 index f9dee95d..00000000 --- a/examples/symbols/pypsa_losses.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -# How `examples/pypsa_losses.yaml` prints — the line symbols of `pypsa.yaml`, plus the loss and its fan. -notation: latex -dimensions: - link: { index: l, set: '\mathcal{L}' } - link_output: { index: o, set: '\mathcal{O}' } - snapshot: { index: t, set: '\mathcal{T}' } - bus: { index: n, set: '\mathcal{N}' } - generator: { index: g, set: '\mathcal{G}' } - load: { index: d, set: '\mathcal{D}' } - line: { index: k, set: '\mathcal{K}' } - cycle: { index: c, set: '\mathcal{C}' } - segment: { index: k, set: '\mathcal{K}' } -names: - Link_p: f - Link_p_nom: '\mathrm{f}^{\mathrm{nom}}' - Link_p_min_pu: '\underline{\mathrm{f}}' - Link_p_max_pu: '\overline{\mathrm{f}}' - Link_efficiency: '\eta' - Link_marginal_cost: '\mathrm{c}^{f}' - snapshot_weightings_objective: '\mathrm{w}' - Generator_p: p - Generator_p_nom: '\mathrm{p}^{\mathrm{nom}}' - Generator_p_min_pu: '\underline{\mathrm{p}}' - Generator_p_max_pu: '\overline{\mathrm{p}}' - Generator_marginal_cost: '\mathrm{c}' - Load_p_set: '\mathrm{load}' - Line_s: s - Line_s_nom_ext: S - Line_s_nom: '\mathrm{s}^{\mathrm{nom}}' - Line_s_nom_extendable: '\mathrm{ext}^{s}' - Line_s_max_pu: '\overline{\mathrm{s}}' - Line_s_nom_min: '\underline{\mathrm{s}}^{\mathrm{nom}}' - Line_s_nom_max: '\overline{\mathrm{s}}^{\mathrm{nom}}' - Line_capital_cost: '\mathrm{c}^{\mathrm{cap},s}' - Line_cycle_weight: '\mathrm{x}' - Line_loss: '\ell' - Line_loss_max: '\overline{\ell}' - Line_loss_slope: '\mathrm{a}' - Line_loss_offset: '\mathrm{b}' diff --git a/mkdocs.yml b/mkdocs.yml index acdf2310..4ded694e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,7 +64,6 @@ nav: - One construct per model: examples/operators.md - PyPSA in one file: examples/pypsa.md - PyPSA, the relaxed commitment: examples/pypsa_linearized_uc.md - - PyPSA, the lossy lines: examples/pypsa_losses.md # Everything a reader does not need in order to write a model: the design # arguments, how to contribute, what changed. - About: @@ -175,7 +174,7 @@ markdown_extensions: permalink: "#" toc_depth: 3 # GitHub's slug rules, not mkdocs'. Every page here is read in both - # places and they cross-link by anchor — `pypsa_losses.md#rung-13 + # places and they cross-link by anchor — `pypsa.md#rung-13 # --transmission-losses`, whose double hyphen is an em dash GitHub # drops. The default slugifier collapses that to one, so such a link # would 404 on the site while working in the repository. Same file, diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index c93d05a4..1dd05b56 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -422,6 +422,17 @@ parameters: Line_expansion_cost_weight: - global_constraint - line + Line_loss_max: + - snapshot + - line + Line_loss_offset: + - snapshot + - line + - segment + Line_loss_slope: + - snapshot + - line + - segment Line_s_max_pu: - snapshot - line @@ -673,6 +684,7 @@ parameters: - snapshot snapshot_weightings_stores: - snapshot + transmission_losses: [] relations: - Generator_bus - Line_bus0 diff --git a/tools/gallery.py b/tools/gallery.py index fc36a48c..1d53c077 100644 --- a/tools/gallery.py +++ b/tools/gallery.py @@ -45,7 +45,6 @@ DECLARED = { 'pypsa.md': ROOT / 'examples' / 'pypsa.yaml', 'pypsa_linearized_uc.md': ROOT / 'examples' / 'pypsa_linearized_uc.yaml', - 'pypsa_losses.md': ROOT / 'examples' / 'pypsa_losses.yaml', } #: One PyPSA reference network per rung, run out of band with the versions From 02c5974453a1739f2d82f6f18ecbebf657f34d27 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 22 Sep 2026 20:54:41 +0200 Subject: [PATCH 05/46] docs(pypsa): the spec covers the secant loss mode --- docs/examples/pypsa.md | 47 +++++++++++++++++++++++++++--------------- examples/pypsa.yaml | 32 +++++++++++++++++++--------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 94b1f84b..6f14bf6a 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1155,11 +1155,20 @@ def build(): ### Rung 13 — transmission losses -`n.optimize(transmission_losses={'mode': 'tangents', 'segments': K})`: a line -dissipates a loss its flow buys along a fan of `K` tangents to the quadratic -loss curve, half charged at either end of the line. The loss variable, its cap -and the tangent rows exist only where `transmission_losses` is on; the tangent -slopes and offsets are data prep, one per `segment`. A plain run leaves the flag +`n.optimize(transmission_losses=...)`: a line dissipates a loss its flow buys, +held above a fan of cuts to the quadratic loss curve `r_pu_eff * p**2`, half +charged at either end of the line. PyPSA has two modes, and both build the same +rows `loss + slope * flow >= offset` and `loss - slope * flow >= offset`, one +pair per cut: `{'mode': 'tangents', 'segments': K}` takes `K` tangents at +`p_k = k / K` of the rating, slope `2 r p_k`; `True`, or +`{'mode': 'secants', 'atol': 1, 'rtol': 0.1, 'max_segments': 20}`, takes the +secants between consecutive breakpoints `p_k, p_k+1`, slope `r (p_k + p_k+1)` +and offset `-r p_k p_k+1`, the breakpoints placed from `p_0 = 0` by a step +`max(k / (k - 1), 1 + 2 (rtol + sqrt(rtol + rtol**2)))` until the rating is +covered (`constraints.py:2545`). The mode therefore only decides how data prep +fills `Line_loss_slope` and `Line_loss_offset` over the `segment` axis, and the +breakpoint loop is data prep with them. The loss variable, its cap and the cut +rows exist only where `transmission_losses` is on. A plain run leaves the flag off and supplies no segments, so the loss is absent and reads as zero in the balance, and the model collapses to the lossless one. @@ -1168,10 +1177,10 @@ balance, and the model collapses to the lossless one. | [`Line-loss`](#variable-domains) | done | absent, and zero in the balance, where lossless | | [`Line-fix-s-*`, `Line-ext-s-*`](#line-fix-s-lower) | done | the loss counted against the rating | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | half of each incident line's loss at either end | -| [`Line-loss_upper`](#line-loss_upper) | done | `loss_max` is data prep | +| [`Line-loss_upper`](#line-loss_upper) | done | `loss_max` is data prep, see X4 | | [`Line-loss_tangents-{k}-1`](#line-loss_tangents-k-1) | split | PyPSA names a row per segment; one block over the dimension | | [`Line-loss_tangents-{k}--1`](#line-loss_tangents-k--1) | split | | -| `Line-loss_secants-*` | out | the secant mode solves for its segment count | +| [`Line-loss_secants-pos`, `Line-loss_secants-neg`](#line-loss_tangents-k-1) | split | the same two blocks, secant slope and offset and the breakpoint loop as data prep; no reference records this mode yet | > ✔ `pypsa 1.3.0` solves this rung's network at objective `10645.295879552297`, 150 rows. @@ -1651,7 +1660,8 @@ data prep, or harness — is one open question. Line numbers are pinned pypsa | `ValueError`, `constraints.py:1557` | load on a bus with nothing attached | row not built, unserved | X2 | | `ValueError`, `optimize.py:436` | no component carries a cost | feasibility problem | X3 | | `NotImplementedError`, `global_constraints.py:457` | depletion with period weightings `!= 1` | out | | -| `ValueError`/`RuntimeError`, losses | `s_nom_max = inf`; secant cap | out | | +| `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` | X4 | +| `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | Duals and solutions are read back by the harness on the lpspec side: `marginal_price` is the balance dual over `w_objective`, `mu_upper` the @@ -1680,7 +1690,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | | $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a fixed phase shift | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | -| $`\mathcal{B}`$ | index $`b`$ — `segment` — the tangents a line's loss curve is approximated by — PyPSA's `transmission_losses` count; none in a lossless run | +| $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a line's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | | $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | | $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | @@ -1817,10 +1827,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | | $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | | $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row | -| $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` count read as a flag, its tangents the `segment` axis; false with no segments is a lossless run | +| $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the same; false with no segments is a lossless run | | $`\overline{\ell}`$ | `Line_loss_max` over $`\mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | -| $`\mathrm{a}`$ | `Line_loss_slope` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a tangent to the loss curve at its segment's flow — `2 * r_pu_eff * p_k`, data prep | -| $`\mathrm{b}`$ | `Line_loss_offset` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — where that tangent meets the loss axis — `loss_k - slope_k * p_k`, negative, data prep | +| $`\mathrm{a}`$ | `Line_loss_slope` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep | +| $`\mathrm{b}`$ | `Line_loss_offset` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | | $`\sigma^{\mathrm{nom}}`$ | `Transformer_s_nom` over $`\mathcal{M}`$ — nominal apparent power | | $`\mathrm{ext}^{\sigma}`$ | `Transformer_s_nom_extendable` over $`\mathcal{M}`$ — whether the nominal apparent power is a decision | | $`\overline{\sigma}`$ | `Transformer_s_max_pu` over $`\mathcal{T} \times \mathcal{M}`$ — most flow either way, per unit of nominal apparent power | @@ -1870,7 +1880,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{up}`$ | `Generator_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | | $`\mathit{dn}`$ | `Generator_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | -| $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the tangents; absent, and zero in the balance, where the network is lossless | +| $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | @@ -2994,9 +3004,10 @@ Line_loss_upper: ```yaml Line_loss_tangents_forward: description: >- - `Line-loss_tangents-{k}-1` — the loss sits above every tangent to its - curve for flow one way; PyPSA names one row per segment `k`, this block - states them all over the segment dimension + `Line-loss_tangents-{k}-1` — the loss sits above every cut to its curve + for flow one way; PyPSA names one row per tangent `k`, or the one row + `Line-loss_secants-pos` over its `secant` axis, this block states them + all over the segment dimension dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active expression: Line_loss + Line_loss_slope * Line_s >= Line_loss_offset @@ -3012,7 +3023,9 @@ Line_loss_tangents_forward: ```yaml Line_loss_tangents_reverse: - description: "`Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending on the flow's magnitude" + description: >- + `Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending + on the flow's magnitude; `Line-loss_secants-neg` in the secant mode dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index e85e1e05..ba1eb746 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -50,7 +50,10 @@ dimensions: cycle: description: independent cycles of the passive network graph — the cycle basis, data prep segment: - description: the tangents a line's loss curve is approximated by — PyPSA's `transmission_losses` count; none in a lossless run + description: >- + the cuts a line's loss curve is held above — PyPSA's tangents, as many as + its `segments` count, or its secants, as many as its tolerance loop + places; none in a lossless run dtype: int global_constraint: description: PyPSA's `GlobalConstraint` rows, one label per declared limit @@ -504,18 +507,24 @@ parameters: transmission_losses: description: >- whether the network dissipates transmission losses — PyPSA's - `transmission_losses` count read as a flag, its tangents the `segment` - axis; false with no segments is a lossless run + `transmission_losses` read as a flag; its mode, tangents or secants, + only decides how data prep fills the `segment` axis, the rows are the + same; false with no segments is a lossless run dims: [] dtype: bool Line_loss_max: description: the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep dims: [snapshot, line] Line_loss_slope: - description: the slope of a tangent to the loss curve at its segment's flow — `2 * r_pu_eff * p_k`, data prep + description: >- + the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` + at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between + consecutive breakpoints, data prep dims: [snapshot, line, segment] Line_loss_offset: - description: where that tangent meets the loss axis — `loss_k - slope_k * p_k`, negative, data prep + description: >- + where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, + a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep dims: [snapshot, line, segment] Transformer_s_nom: description: nominal apparent power @@ -789,7 +798,7 @@ variables: Line_loss: description: >- `Line-loss` — what a line dissipates carrying its flow, pushed down by the - cost and held up by the tangents; absent, and zero in the balance, where + cost and held up by the cuts; absent, and zero in the balance, where the network is lossless dims: [scenario, snapshot, line] where: transmission_losses AND Line_active @@ -1407,14 +1416,17 @@ constraints: expression: Line_loss <= Line_loss_max Line_loss_tangents_forward: description: >- - `Line-loss_tangents-{k}-1` — the loss sits above every tangent to its - curve for flow one way; PyPSA names one row per segment `k`, this block - states them all over the segment dimension + `Line-loss_tangents-{k}-1` — the loss sits above every cut to its curve + for flow one way; PyPSA names one row per tangent `k`, or the one row + `Line-loss_secants-pos` over its `secant` axis, this block states them + all over the segment dimension dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active expression: Line_loss + Line_loss_slope * Line_s >= Line_loss_offset Line_loss_tangents_reverse: - description: "`Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending on the flow's magnitude" + description: >- + `Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending + on the flow's magnitude; `Line-loss_secants-neg` in the secant mode dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset From 0ce824733c9e35cfcaf9bd16cd6dfdaa61c7dbd0 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 22 Sep 2026 21:05:16 +0200 Subject: [PATCH 06/46] 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/examples/pypsa.md | 65 +++++++++++++++++-- examples/pypsa.yaml | 10 +-- examples/references/pypsa/references.json | 31 +++++++++ .../pypsa/rung_19_losses_secants.py | 36 ++++++++++ tests/test_pypsa_references.py | 10 ++- tools/gallery.py | 21 ++++-- 6 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 examples/references/pypsa/rung_19_losses_secants.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 6f14bf6a..be7bd474 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1180,7 +1180,7 @@ balance, and the model collapses to the lossless one. | [`Line-loss_upper`](#line-loss_upper) | done | `loss_max` is data prep, see X4 | | [`Line-loss_tangents-{k}-1`](#line-loss_tangents-k-1) | split | PyPSA names a row per segment; one block over the dimension | | [`Line-loss_tangents-{k}--1`](#line-loss_tangents-k--1) | split | | -| [`Line-loss_secants-pos`, `Line-loss_secants-neg`](#line-loss_tangents-k-1) | split | the same two blocks, secant slope and offset and the breakpoint loop as data prep; no reference records this mode yet | +| [`Line-loss_secants-pos`, `Line-loss_secants-neg`](#line-loss_tangents-k-1) | done | the same two blocks in the secant mode; slope, offset and the breakpoint loop are data prep; rung 19 records it | > ✔ `pypsa 1.3.0` solves this rung's network at objective `10645.295879552297`, 150 rows. @@ -1232,6 +1232,59 @@ def build(): +The same triangle solved in the secant mode records the identical loss rows, +its cuts placed by PyPSA's tolerance loop rather than fixed per segment. + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `10840.926895402912`, 150 rows. + +
+The network, as PyPSA code + +`rung_19_losses_secants.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 19: transmission losses in secant form — the same loss per line, its cuts placed by PyPSA's tolerance loop.""" + +from __future__ import annotations + +import spine + +OPTIMIZE = {'transmission_losses': {'mode': 'secants', 'atol': 1, 'rtol': 0.1, 'max_segments': 20}} + + +def build(): + """Rung 13's 110 kV triangle, unchanged, so the two modes differ only in the cuts.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c'], v_nom=110) + n.add('Generator', 'hydro19', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel19', bus='b', p_nom=80, marginal_cost=50) + n.add('Line', 'ab19', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) + n.add('Line', 'bc19', bus0='b', bus1='c', carrier='AC', x=60, r=9.7, s_nom=60) + n.add( + 'Line', + 'ca19', + bus0='c', + bus1='a', + carrier='AC', + x=45, + r=6, + s_nom=40, + s_nom_extendable=True, + s_nom_max=90, + capital_cost=4, + ) + n.add('Load', 'town19', bus='c', p_set=[35, 55, 15, 45]) + return n +``` + +
+ + ### Rung 14 — two-stage stochastic Two futures and a risk preference: `n.set_scenarios(...)` with @@ -3004,9 +3057,9 @@ Line_loss_upper: ```yaml Line_loss_tangents_forward: description: >- - `Line-loss_tangents-{k}-1` — the loss sits above every cut to its curve - for flow one way; PyPSA names one row per tangent `k`, or the one row - `Line-loss_secants-pos` over its `secant` axis, this block states them + `Line-loss_tangents-{k}-1`, `Line-loss_secants-pos` — the loss sits above + every cut to its curve for flow one way; PyPSA names one row per tangent + `k`, or one row stacked over its `secant` axis, and this block states them all over the segment dimension dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active @@ -3024,8 +3077,8 @@ Line_loss_tangents_forward: ```yaml Line_loss_tangents_reverse: description: >- - `Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending - on the flow's magnitude; `Line-loss_secants-neg` in the secant mode + `Line-loss_tangents-{k}--1`, `Line-loss_secants-neg` — the same fan + mirrored, the loss depending on the flow's magnitude dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index ba1eb746..efbb2c8a 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -1416,17 +1416,17 @@ constraints: expression: Line_loss <= Line_loss_max Line_loss_tangents_forward: description: >- - `Line-loss_tangents-{k}-1` — the loss sits above every cut to its curve - for flow one way; PyPSA names one row per tangent `k`, or the one row - `Line-loss_secants-pos` over its `secant` axis, this block states them + `Line-loss_tangents-{k}-1`, `Line-loss_secants-pos` — the loss sits above + every cut to its curve for flow one way; PyPSA names one row per tangent + `k`, or one row stacked over its `secant` axis, and this block states them all over the segment dimension dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active expression: Line_loss + Line_loss_slope * Line_s >= Line_loss_offset Line_loss_tangents_reverse: description: >- - `Line-loss_tangents-{k}--1` — the same fan mirrored, the loss depending - on the flow's magnitude; `Line-loss_secants-neg` in the secant mode + `Line-loss_tangents-{k}--1`, `Line-loss_secants-neg` — the same fan + mirrored, the loss depending on the flow's magnitude dims: [scenario, snapshot, line, segment] where: transmission_losses AND Line_active expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index ac9fd5dd..11dfd56a 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -643,5 +643,36 @@ "Transformer-s_nom_set": 1, "Transformer-s_set": 1 } + }, + "rung_19_losses_secants": { + "columns": { + "Generator-p": 16, + "Line-loss": 12, + "Line-s": 12, + "Line-s_nom": 1, + "Link-p": 4, + "objective_constant": 1 + }, + "global_constraints": {}, + "objective": 10840.926895402912, + "objective_constant": 160.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 20, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Kirchhoff-Voltage-Law": 4, + "Line-ext-s-lower": 4, + "Line-ext-s-upper": 4, + "Line-ext-s_nom-lower": 1, + "Line-ext-s_nom-upper": 1, + "Line-fix-s-lower": 8, + "Line-fix-s-upper": 8, + "Line-loss_secants-neg": 24, + "Line-loss_secants-pos": 24, + "Line-loss_upper": 12, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_19_losses_secants.py b/examples/references/pypsa/rung_19_losses_secants.py new file mode 100644 index 00000000..81fedb1d --- /dev/null +++ b/examples/references/pypsa/rung_19_losses_secants.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 19: transmission losses in secant form — the same loss per line, its cuts placed by PyPSA's tolerance loop.""" + +from __future__ import annotations + +import spine + +OPTIMIZE = {'transmission_losses': {'mode': 'secants', 'atol': 1, 'rtol': 0.1, 'max_segments': 20}} + + +def build(): + """Rung 13's 110 kV triangle, unchanged, so the two modes differ only in the cuts.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c'], v_nom=110) + n.add('Generator', 'hydro19', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel19', bus='b', p_nom=80, marginal_cost=50) + n.add('Line', 'ab19', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) + n.add('Line', 'bc19', bus0='b', bus1='c', carrier='AC', x=60, r=9.7, s_nom=60) + n.add( + 'Line', + 'ca19', + bus0='c', + bus1='a', + carrier='AC', + x=45, + r=6, + s_nom=40, + s_nom_extendable=True, + s_nom_max=90, + capital_cost=4, + ) + n.add('Load', 'town19', bus='c', p_set=[35, 55, 15, 45]) + return n diff --git a/tests/test_pypsa_references.py b/tests/test_pypsa_references.py index 3e1fcabd..aae55ab7 100644 --- a/tests/test_pypsa_references.py +++ b/tests/test_pypsa_references.py @@ -19,7 +19,7 @@ from math_spec import to_spec from tools import gallery -from tools.gallery import DECLARED, RECORDED, REFERENCES, _stands_for +from tools.gallery import DECLARED, RECORDED, REFERENCES, _names_for, _stands_for RUNGS = sorted(path.stem for path in REFERENCES.glob('rung_*.py')) SCRIPT = REFERENCES / 'reference.py' @@ -28,8 +28,12 @@ SPECS = {page: to_spec(path) for page, path in DECLARED.items()} MODELS = list(SPECS.values()) BASE = SPECS['pypsa.md'] -ROWS_DECLARED = {_stands_for(name, block.description) for m in MODELS for name, block in m.constraints.items()} -COLUMNS_DECLARED = {_stands_for(name, block.description) for m in MODELS for name, block in m.variables.items()} +ROWS_DECLARED = { + n for m in MODELS for name, block in m.constraints.items() for n in _names_for(name, block.description) +} +COLUMNS_DECLARED = { + n for m in MODELS for name, block in m.variables.items() for n in _names_for(name, block.description) +} #: The five GlobalConstraint formulas open with their *type* — PyPSA names #: those rows after each row's own label, so they are matched through the #: recorded type and sense instead of by name. diff --git a/tools/gallery.py b/tools/gallery.py index 1d53c077..5234db78 100644 --- a/tools/gallery.py +++ b/tools/gallery.py @@ -86,15 +86,26 @@ def declaration(text: str, section: str, name: str | None = None) -> str: return textwrap.dedent('\n'.join(lines[i:j])).rstrip() -def _stands_for(name: str, description: str | None) -> str: - """The other side's name for a declaration — the backticked opening of its description.""" - found = re.match(r'`([^`]+)`', description or '') - if found is None: +def _names_for(name: str, description: str | None) -> list[str]: + """Every other-side name a declaration stands for: the backticked tokens before the ` — ` of its description. + + One declaration answers to one PyPSA name as a rule; a block whose rows PyPSA + names differently by mode lists them all before the dash, the first canonical. + """ + text = description or '' + head = text.split(' — ', 1)[0] if ' — ' in text else (re.match(r'`[^`]+`', text) or [''])[0] + names = re.findall(r'`([^`]+)`', head) + if not names: msg = ( f'{name}: a declaration on a declared page opens its description with the name it stands for, in backticks' ) raise ValueError(msg) - return found.group(1) + return names + + +def _stands_for(name: str, description: str | None) -> str: + """The other side's canonical name for a declaration — the backticked opening of its description.""" + return _names_for(name, description)[0] def declared_block(path: Path) -> str: From b0fba215be13dd5398125d7b9420b39b83f0bd5d Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 22 Sep 2026 21:26:55 +0200 Subject: [PATCH 07/46] 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/examples/pypsa.md | 101 +++++++++++++++--- examples/pypsa.yaml | 55 ++++++++-- examples/references/pypsa/references.json | 25 +++++ .../references/pypsa/rung_20_phase_shifter.py | 38 +++++++ tests/fixtures/pypsa_standard_shape.yaml | 9 ++ 5 files changed, 208 insertions(+), 20 deletions(-) create mode 100644 examples/references/pypsa/rung_20_phase_shifter.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index be7bd474..66e0310b 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1607,12 +1607,12 @@ def build(): ### Rung 18 — transformer A transformer is a passive branch between two buses, as a line is, but its flow -follows its effective series reactance and a fixed phase shift. It obeys the -Kirchhoff voltage law (KVL) around every independent cycle, so it builds no flow -outside a mesh. A non-extendable transformer carries a fixed nominal apparent -power. An extendable one chooses it between `s_nom_min` and `s_nom_max`. An -`s_set` fixes a flow schedule. An `s_nom_set` fixes an extendable transformer's -built capacity. +follows its effective series reactance and a phase shift, fixed or optimised. It +obeys the Kirchhoff voltage law (KVL) around every independent cycle, so it +builds no flow outside a mesh. A non-extendable transformer carries a fixed +nominal apparent power. An extendable one chooses it between `s_nom_min` and +`s_nom_max`. An `s_set` fixes a flow schedule. An `s_nom_set` fixes an extendable +transformer's built capacity. | PyPSA | status | note | | --- | --- | --- | @@ -1621,6 +1621,7 @@ built capacity. | [`Transformer-s_set`](#transformer-s_set) | done | a fixed flow schedule | | [`Transformer-s_nom_set`](#transformer-s_nom_set) | done | a fixed built capacity | | [`Kirchhoff-Voltage-Law`](#kirchhoff-voltage-law) | done | rung 6, over `x_pu_eff` and a phase shift | +| [`Transformer-phase_shift`](#variable-domains) | done | rung 20, an optimised phase shift | | [objective](#objective) | done | capital on capacity | @@ -1700,6 +1701,67 @@ def build(): +### Rung 20 — phase shifter + +A phase-shifting transformer's voltage angle shift is a per-snapshot decision +where its `phase_shift_min` sits below its `phase_shift_max`, bounded between the +two in degrees. The shift enters the same KVL cycle sum as a fixed one, so it +redistributes the flows around a cycle without moving active power. Here the +shift holds the transformer at its rating while the upstream unit serves the +whole varying load, and the fixed `phase_shift` gives way to it. + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `16455.0`, 88 rows. + +
+The network, as PyPSA code + +`rung_20_phase_shifter.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 20: phase shifter — a transformer whose per-snapshot phase shift is optimised, holding it at its rating and rerouting the surplus around the cycle. + +The parallel lines carry low reactance, so a few degrees of shift move tens of +megawatts: the phase-shifting transformer keeps its flow at its ``s_nom`` while +the upstream hydro serves the whole varying load, and the costly local unit +stays off. A fixed shift could not follow the load, so the shift is a decision. +""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a triangle where a phase-shifting transformer reroutes cheap power around a binding leg, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c']) + n.add('Generator', 'hydro20', bus='a', p_nom=300, marginal_cost=10) + n.add('Generator', 'diesel20', bus='c', p_nom=300, marginal_cost=200) + n.add('Load', 'town20', bus='c', p_set=[90, 75, 120, 105]) + n.add('Line', 'ab20', bus0='a', bus1='b', carrier='AC', x=0.002, r=0.0002, s_nom=120) + n.add('Line', 'bc20', bus0='b', bus1='c', carrier='AC', x=0.002, r=0.0002, s_nom=120) + n.add( + 'Transformer', + 'ca20', + bus0='c', + bus1='a', + x=0.002, + r=0.0002, + s_nom=40, + phase_shift_min=-30, + phase_shift_max=30, + ) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -1741,7 +1803,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | | $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | | $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | -| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a fixed phase shift | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | | $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a line's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | @@ -1893,7 +1955,11 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$ — a given nominal apparent power for an extendable transformer; one without a value has no row here | | $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | | $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row | -| $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's fixed phase shift in radians, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; a transformer with no shift or in no cycle has no row. PyPSA also admits an optimisable phase shift, a later rung this file does not carry | +| $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{M} \times \mathcal{C}`$ — a fixed transformer's phase shift in radians, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; zero for a varying transformer, whose shift is a decision instead, so the constant and the variable term never both count a shift. A transformer with no shift or in no cycle has no row | +| $`\mathrm{Transformer\_phase\_shift\_varying}`$ | `Transformer_phase_shift_varying` over $`\mathcal{M}`$ — whether a transformer's phase shift is a decision — PyPSA's `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a fixed shift carried by `phase_shift` | +| $`\mathrm{Transformer\_phase\_shift\_min}`$ | `Transformer_phase_shift_min` over $`\mathcal{M}`$ — the least a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_min`; where it is below `phase_shift_max` the shift is a decision, otherwise the transformer keeps its fixed `phase_shift` | +| $`\mathrm{Transformer\_phase\_shift\_max}`$ | `Transformer_phase_shift_max` over $`\mathcal{M}`$ — the most a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_max`; equal to `phase_shift_min` for a fixed transformer | +| $`\mathrm{Transformer\_phase\_shift\_cycle\_weight}`$ | `Transformer_phase_shift_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the cycle sign for a varying transformer's phase shift, times π/180 so a shift in degrees enters the cycle sum in radians — data prep; zero for a fixed transformer or one in no cycle | | $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{I}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | | $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\mathcal{I}`$ — which way the row binds — `<=`, `>=` or `==` | | $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, a non-extendable build — is folded in here by data prep | @@ -1935,6 +2001,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | | $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | +| $`\mathit{Transformer\_phase\_shift}`$ | `Transformer_phase_shift` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-phase_shift` — a phase-shifting transformer's voltage angle shift in degrees, chosen per snapshot to redistribute the flows around its cycles without moving active power; absent, and zero in the cycle sum, where the shift is fixed | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`F`$ | `Link_p_nom_ext` over $`\mathcal{L}`$ — `Link-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | @@ -1968,6 +2035,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | +Upright is what the model is given — a parameter such as $`\mathrm{Transformer\_phase\_shift\_varying}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{Transformer\_phase\_shift}`$. An index is italic too, being what a quantifier chooses, and a set is script. + $`t \ominus k`$ denotes cyclic translation: index $`t-k`$ taken modulo the size of the dimension (`roll`). Plain $`t-k`$ (`shift`) has no wraparound — terms translated past the edge are simply absent. $`t \boxminus_{v} k`$ denotes translation with $`v`$ standing where index $`t-k`$ leaves the dimension (`shift(edge=v)`), so the row at that boundary is built and carries $`v`$ rather than being dropped. @@ -3226,17 +3295,19 @@ Kirchhoff_Voltage_Law: `Kirchhoff-Voltage-Law` — around every independent cycle the impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport. A transformer's flow weighs its - effective reactance, and its fixed phase shift enters the cycle sum as a - constant + effective reactance, and its phase shift enters the cycle sum too: a + constant where the shift is fixed, or the shift decision times its cycle + weight where the shift is a phase-shifting transformer's to choose dims: [scenario, snapshot, cycle] expression: >- sum(Line_s * Line_cycle_weight, over=line) + sum(Transformer_s * Transformer_cycle_weight, over=transformer) - + sum(Transformer_phase_shift_weight, over=transformer) == 0 + + sum(Transformer_phase_shift_weight, over=transformer) + + sum(Transformer_phase_shift * Transformer_phase_shift_cycle_weight, over=transformer) == 0 ``` ```math -\sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{k,c} + \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{m,c} + \sum_{m \in \mathcal{M}} \vartheta_{m,c} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ c \in \mathcal{C} +\sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{k,c} + \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{m,c} + \sum_{m \in \mathcal{M}} \vartheta_{m,c} + \sum_{m \in \mathcal{M}} \mathit{Transformer\_phase\_shift}_{\xi,t,m} \cdot \mathrm{Transformer\_phase\_shift\_cycle\_weight}_{m,c} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ c \in \mathcal{C} ``` ### `Generator-p-ramp_limit_up` @@ -4507,6 +4578,12 @@ s_{\xi,t,k} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \sigma_{\xi,t,m} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{on}^{\sigma}_{t,m} ``` +**`Transformer_phase_shift`** + +```math +\mathrm{Transformer\_phase\_shift\_min}_{m} \le \mathit{Transformer\_phase\_shift}_{\xi,t,m} \le \mathrm{Transformer\_phase\_shift\_max}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{Transformer\_phase\_shift\_varying}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + **`Line_s_nom_ext`** ```math diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index efbb2c8a..d7c751aa 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -46,7 +46,7 @@ dimensions: line: description: passive branches, each between two buses, their flow set by impedance transformer: - description: passive branches between two buses, their flow set by impedance and tap ratio, with a fixed phase shift + description: passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised cycle: description: independent cycles of the passive network graph — the cycle basis, data prep segment: @@ -559,10 +559,35 @@ parameters: dims: [transformer, cycle] Transformer_phase_shift_weight: description: >- - the transformer's fixed phase shift in radians, signed by its orientation - in the cycle — a constant added to the cycle sum, data prep; a transformer - with no shift or in no cycle has no row. PyPSA also admits an optimisable - phase shift, a later rung this file does not carry + a fixed transformer's phase shift in radians, signed by its orientation + in the cycle — a constant added to the cycle sum, data prep; zero for a + varying transformer, whose shift is a decision instead, so the constant and + the variable term never both count a shift. A transformer with no shift or + in no cycle has no row + dims: [transformer, cycle] + Transformer_phase_shift_varying: + description: >- + whether a transformer's phase shift is a decision — PyPSA's + `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a + fixed shift carried by `phase_shift` + dims: [transformer] + dtype: bool + Transformer_phase_shift_min: + description: >- + the least a varying transformer's phase shift may take, in degrees — + PyPSA's `phase_shift_min`; where it is below `phase_shift_max` the shift is + a decision, otherwise the transformer keeps its fixed `phase_shift` + dims: [transformer] + Transformer_phase_shift_max: + description: >- + the most a varying transformer's phase shift may take, in degrees — + PyPSA's `phase_shift_max`; equal to `phase_shift_min` for a fixed transformer + dims: [transformer] + Transformer_phase_shift_cycle_weight: + description: >- + the cycle sign for a varying transformer's phase shift, times π/180 so a + shift in degrees enters the cycle sum in radians — data prep; zero for a + fixed transformer or one in no cycle dims: [transformer, cycle] GlobalConstraint_type: description: >- @@ -812,6 +837,18 @@ variables: `Transformer_bus1`, lossless dims: [scenario, snapshot, transformer] where: Transformer_active + Transformer_phase_shift: + description: >- + `Transformer-phase_shift` — a phase-shifting transformer's voltage angle + shift in degrees, chosen per snapshot to redistribute the flows around its + cycles without moving active power; absent, and zero in the cycle sum, + where the shift is fixed + dims: [scenario, snapshot, transformer] + where: Transformer_phase_shift_varying AND Transformer_active + absence: zero + bounds: + lower: Transformer_phase_shift_min + upper: Transformer_phase_shift_max Line_s_nom_ext: description: >- `Line-s_nom` — nominal apparent power where it is a decision; the @@ -1475,13 +1512,15 @@ constraints: `Kirchhoff-Voltage-Law` — around every independent cycle the impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport. A transformer's flow weighs its - effective reactance, and its fixed phase shift enters the cycle sum as a - constant + effective reactance, and its phase shift enters the cycle sum too: a + constant where the shift is fixed, or the shift decision times its cycle + weight where the shift is a phase-shifting transformer's to choose dims: [scenario, snapshot, cycle] expression: >- sum(Line_s * Line_cycle_weight, over=line) + sum(Transformer_s * Transformer_cycle_weight, over=transformer) - + sum(Transformer_phase_shift_weight, over=transformer) == 0 + + sum(Transformer_phase_shift_weight, over=transformer) + + sum(Transformer_phase_shift * Transformer_phase_shift_cycle_weight, over=transformer) == 0 Generator_p_ramp_limit_up: description: >- `Generator-p-ramp_limit_up` — a generator raises output no faster than diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 11dfd56a..c124d27c 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -674,5 +674,30 @@ "Link-fix-p-lower": 4, "Link-fix-p-upper": 4 } + }, + "rung_20_phase_shifter": { + "columns": { + "Generator-p": 16, + "Line-s": 8, + "Link-p": 4, + "Transformer-phase_shift": 4, + "Transformer-s": 4 + }, + "global_constraints": {}, + "objective": 16455.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 20, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Kirchhoff-Voltage-Law": 4, + "Line-fix-s-lower": 8, + "Line-fix-s-upper": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Transformer-fix-s-lower": 4, + "Transformer-fix-s-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_20_phase_shifter.py b/examples/references/pypsa/rung_20_phase_shifter.py new file mode 100644 index 00000000..efe88b1f --- /dev/null +++ b/examples/references/pypsa/rung_20_phase_shifter.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 20: phase shifter — a transformer whose per-snapshot phase shift is optimised, holding it at its rating and rerouting the surplus around the cycle. + +The parallel lines carry low reactance, so a few degrees of shift move tens of +megawatts: the phase-shifting transformer keeps its flow at its ``s_nom`` while +the upstream hydro serves the whole varying load, and the costly local unit +stays off. A fixed shift could not follow the load, so the shift is a decision. +""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a triangle where a phase-shifting transformer reroutes cheap power around a binding leg, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c']) + n.add('Generator', 'hydro20', bus='a', p_nom=300, marginal_cost=10) + n.add('Generator', 'diesel20', bus='c', p_nom=300, marginal_cost=200) + n.add('Load', 'town20', bus='c', p_set=[90, 75, 120, 105]) + n.add('Line', 'ab20', bus0='a', bus1='b', carrier='AC', x=0.002, r=0.0002, s_nom=120) + n.add('Line', 'bc20', bus0='b', bus1='c', carrier='AC', x=0.002, r=0.0002, s_nom=120) + n.add( + 'Transformer', + 'ca20', + bus0='c', + bus1='a', + x=0.002, + r=0.0002, + s_nom=40, + phase_shift_min=-30, + phase_shift_max=30, + ) + return n diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index 1dd05b56..7e66c9bd 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -657,6 +657,15 @@ parameters: Transformer_cycle_weight: - transformer - cycle + Transformer_phase_shift_cycle_weight: + - transformer + - cycle + Transformer_phase_shift_max: + - transformer + Transformer_phase_shift_min: + - transformer + Transformer_phase_shift_varying: + - transformer Transformer_phase_shift_weight: - transformer - cycle From c125c565addfbf59411ba470ab3721a8725c573e Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 09:21:51 +0200 Subject: [PATCH 08/46] docs(pypsa): the carrier growth limit binds every extendable component --- docs/examples/pypsa.md | 159 ++++++++++++++++-- examples/pypsa.yaml | 68 +++++++- examples/references/pypsa/references.json | 40 ++++- .../pypsa/rung_21_carrier_growth.py | 69 ++++++++ examples/symbols/pypsa.yaml | 6 + 5 files changed, 316 insertions(+), 26 deletions(-) create mode 100644 examples/references/pypsa/rung_21_carrier_growth.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 66e0310b..7556d91b 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -205,7 +205,7 @@ def build(): | [capital cost](#objective) | done | `periodized_cost` is an annuity, data prep | -> ✔ `pypsa 1.3.0` solves this rung's network at objective `7633.908502024291`, 184 rows. +> ✔ `pypsa 1.3.0` solves this rung's network at objective `7633.908502024292`, 184 rows.
The network, as PyPSA code @@ -419,10 +419,10 @@ each type is three blocks by sense. | [`transmission_expansion_cost_limit`](#transmission_expansion_cost_limit) | split | a block per sense | | [`tech_capacity_expansion_limit`](#tech_capacity_expansion_limit) | split | a block per sense | | `Bus-nom_min/max_{carrier}` | out | deprecated in PyPSA | -| [`Carrier-growth_limit`](#carrier-growth_limit) | done | rung 15, below | +| [`Carrier-growth_limit`](#carrier-growth_limit) | done | generators in rung 15, every extendable component in rung 21, below | -> ✔ `pypsa 1.3.0` solves this rung's network at objective `10282.833333333332`, 102 rows. +> ✔ `pypsa 1.3.0` solves this rung's network at objective `10282.833333333334`, 102 rows.
The network, as PyPSA code @@ -1354,7 +1354,7 @@ own dimensions. | --- | --- | --- | | [`Generator-p`](#variable-domains) | done | where the generator stands in the snapshot's period — `active`, data prep | | [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | -| [`Carrier-growth_limit`](#carrier-growth_limit) | done | counted in the first period a build stands in; `edge=0` at the first period | +| [`Carrier-growth_limit`](#carrier-growth_limit) | done | every extendable component of the carrier, counted in the first period a build stands in; `edge=0` at the first period | | [objective](#objective) | done | period weight on operation; capacity once per period it stands in | | `StorageUnit-energy_balance` per period, ramps at period starts | out | `shift(…, by=snapshot_period, within=period)` has them; a later rung | @@ -1762,6 +1762,99 @@ def build():
+### Rung 21 — carrier growth + +A carrier's `max_growth` caps what it adds in an investment period, across every +extendable component of that carrier, not the generators alone. Here a battery +carrier caps a storage unit and a store built in the first period, and the two +builds fill the cap together. A store built in the later period adds its +allowance plus half of what the carrier added before. PyPSA counts the +components that carry a carrier attribute; the spec extends the same limit to +transformers, which PyPSA leaves out. + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `8452.5`, 74 rows. + +
+The network, as PyPSA code + +`rung_21_carrier_growth.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 21: a carrier's growth limit binds its non-generator builds — a storage unit and two stores over two periods.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: cheap solar only at the first snapshot of each period, so battery capacity pays off but its growth is capped.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', 'grid') + n.add('Carrier', 'solar') + n.add('Carrier', 'gas') + n.add('Carrier', 'battery', max_growth=20, max_relative_growth=0.5) + n.add('Generator', 'solar', bus='grid', carrier='solar', p_nom=100, marginal_cost=1, p_max_pu=[1, 0, 1, 0]) + n.add('Generator', 'backup', bus='grid', carrier='gas', p_nom=200, marginal_cost=80) + n.add( + 'StorageUnit', + 'bat20', + bus='grid', + carrier='battery', + p_nom_extendable=True, + p_nom_max=100, + max_hours=4, + capital_cost=50, + build_year=2020, + lifetime=30, + ) + n.add( + 'Store', + 'tank20', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=10, + capital_cost=10, + build_year=2020, + lifetime=30, + ) + n.add( + 'Store', + 'tank30', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=100, + capital_cost=8, + build_year=2030, + lifetime=30, + ) + n.add('Load', 'town', bus='grid', p_set=[40, 60, 40, 80]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -1795,20 +1888,20 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | | $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | +| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | | $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | +| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | | $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | -| $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | -| $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | -| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | -| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | +| $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | +| $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | +| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | | $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a line's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | | $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -1883,6 +1976,12 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{W}^{z}`$ | `Process_capital_weight` over $`\mathcal{J}`$ — the sum of period weights a process stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{W}^{\sigma}`$ | `Transformer_capital_weight` over $`\mathcal{M}`$ — the sum of period weights a transformer stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\mathrm{new}^{f}`$ | `Link_first_active` over $`\mathcal{Y} \times \mathcal{L}`$ — one in the first period a link stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\mathrm{new}^{h}`$ | `StorageUnit_first_active` over $`\mathcal{Y} \times \mathcal{S}`$ — one in the first period a storage unit stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\mathrm{new}^{e}`$ | `Store_first_active` over $`\mathcal{Y} \times \mathcal{V}`$ — one in the first period a store stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\mathrm{new}^{\sigma}`$ | `Transformer_first_active` over $`\mathcal{Y} \times \mathcal{M}`$ — one in the first period a transformer stands in, zero elsewhere — the spec extends the carrier growth limit to transformers, which PyPSA does not, so PyPSA has no counterpart, data prep | | $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit | | $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top | | $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | @@ -2034,6 +2133,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\mathcal{I}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | +| $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. PyPSA sums the components that carry a carrier attribute; the transformer term is the spec's own extension, since PyPSA gives a transformer no carrier | Upright is what the model is given — a parameter such as $`\mathrm{Transformer\_phase\_shift\_varying}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{Transformer\_phase\_shift}`$. An index is italic too, being what a quantifier chooses, and a set is script. @@ -4108,20 +4208,20 @@ Bus_nodal_balance: ```yaml Carrier_growth_limit: description: >- - `Carrier-growth_limit` — what a carrier adds in a period, counting each build in the first - period it stands in, is at most its allowance plus a share of what it added the period before; - the first period has no predecessor, so `edge=0` leaves it the bare allowance + `Carrier-growth_limit` — what a carrier adds across its extendable components in a period, + counting each build in the first period it stands in, is at most its allowance plus a share of + what it added the period before; the first period has no predecessor, so `edge=0` leaves it the + bare allowance dims: [carrier, period] where: Carrier_max_growth expression: >- - sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) - - shift(sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier), along=period, offset=1, edge=0) - * Carrier_max_relative_growth + Carrier_additions + - shift(Carrier_additions, along=period, offset=1, edge=0) * Carrier_max_relative_growth <= Carrier_max_growth ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} - \left( \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y \boxminus_{0} 1,g} \right) \cdot \mathrm{r}_{i} \le \overline{\Delta}_{i} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \overline{\Delta}_{i} \text{ is defined} +\mathit{Carrier\_additions}_{y,i} - \mathit{Carrier\_additions}_{y \boxminus_{0} 1,i} \cdot \mathrm{r}_{i} \le \overline{\Delta}_{i} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \overline{\Delta}_{i} \text{ is defined} ``` ### `CVaR-excess-{s}` @@ -4480,6 +4580,29 @@ scenario_opex: \mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi ``` +### `Carrier_additions` + +```yaml +Carrier_additions: + description: >- + what a carrier adds in a period — every extendable component of that + carrier, counting each build in the first period it stands in. PyPSA sums + the components that carry a carrier attribute; the transformer term is the + spec's own extension, since PyPSA gives a transformer no carrier + expression: >- + sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) + + sum(Link_p_nom_ext * Link_first_active, by=Link_carrier, over=link, into=carrier) + + sum(StorageUnit_p_nom_ext * StorageUnit_first_active, by=StorageUnit_carrier, over=storage_unit, into=carrier) + + sum(Store_e_nom_ext * Store_first_active, by=Store_carrier, over=store, into=carrier) + + sum(Line_s_nom_ext * Line_first_active, by=Line_carrier, over=line, into=carrier) + + sum(Process_p_nom_ext * Process_first_active, by=Process_carrier, over=process, into=carrier) + + sum(Transformer_s_nom_ext * Transformer_first_active, by=Transformer_carrier, over=transformer, into=carrier) +``` + +```math +\mathit{Carrier\_additions}_{y,i} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} + \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} + \sum_{j \in \mathcal{J} \,:\, \mathrm{Process\_carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_carrier}(m) = i} \Sigma_{m} \cdot \mathrm{new}^{\sigma}_{y,m} \qquad \forall\, y \in \mathcal{Y},\ i \in \mathcal{I} +``` + #### Variable domains **`Generator_p`** diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index d7c751aa..594ff0b1 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -320,6 +320,24 @@ parameters: Generator_first_active: description: one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep dims: [period, generator] + Link_first_active: + description: one in the first period a link stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + dims: [period, link] + StorageUnit_first_active: + description: one in the first period a storage unit stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + dims: [period, storage_unit] + Store_first_active: + description: one in the first period a store stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + dims: [period, store] + Line_first_active: + description: one in the first period a line stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + dims: [period, line] + Process_first_active: + description: one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + dims: [period, process] + Transformer_first_active: + description: one in the first period a transformer stands in, zero elsewhere — the spec extends the carrier growth limit to transformers, which PyPSA does not, so PyPSA has no counterpart, data prep + dims: [period, transformer] Carrier_max_growth: description: most capacity of a carrier that may be added in a period; no value means no limit dims: [carrier] @@ -678,6 +696,10 @@ relations: description: the bus a generator sits on key: generator values: bus + Link_carrier: + description: the carrier a link converts from + key: link + values: carrier Link_bus0: description: the bus a link leaves key: link @@ -693,6 +715,10 @@ relations: relation, so the file states any number of them key: link_output values: bus + Process_carrier: + description: the carrier a process converts from + key: process + values: carrier Process_output_process: description: the process a port belongs to key: process_output @@ -708,10 +734,18 @@ relations: description: the bus a load sits on key: load values: bus + StorageUnit_carrier: + description: the carrier a storage unit converts from + key: storage_unit + values: carrier StorageUnit_bus: description: the bus a storage unit sits on key: storage_unit values: bus + Line_carrier: + description: the carrier a line carries + key: line + values: carrier Line_bus0: description: the bus a line's flow is measured at key: line @@ -720,10 +754,18 @@ relations: description: the bus at a line's other end key: line values: bus + Store_carrier: + description: the carrier a store holds + key: store + values: carrier Store_bus: description: the bus a store sits on key: store values: bus + Transformer_carrier: + description: the carrier a transformer carries + key: transformer + values: carrier Transformer_bus0: description: the bus a transformer's flow is measured at key: transformer @@ -1077,6 +1119,20 @@ expressions: + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_shut_down * Generator_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + Carrier_additions: + description: >- + what a carrier adds in a period — every extendable component of that + carrier, counting each build in the first period it stands in. PyPSA sums + the components that carry a carrier attribute; the transformer term is the + spec's own extension, since PyPSA gives a transformer no carrier + expression: >- + sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) + + sum(Link_p_nom_ext * Link_first_active, by=Link_carrier, over=link, into=carrier) + + sum(StorageUnit_p_nom_ext * StorageUnit_first_active, by=StorageUnit_carrier, over=storage_unit, into=carrier) + + sum(Store_e_nom_ext * Store_first_active, by=Store_carrier, over=store, into=carrier) + + sum(Line_s_nom_ext * Line_first_active, by=Line_carrier, over=line, into=carrier) + + sum(Process_p_nom_ext * Process_first_active, by=Process_carrier, over=process, into=carrier) + + sum(Transformer_s_nom_ext * Transformer_first_active, by=Transformer_carrier, over=transformer, into=carrier) constraints: Generator_fix_p_lower: @@ -1808,15 +1864,15 @@ constraints: == sum(Load_p_set, by=Load_bus, over=load, into=bus) Carrier_growth_limit: description: >- - `Carrier-growth_limit` — what a carrier adds in a period, counting each build in the first - period it stands in, is at most its allowance plus a share of what it added the period before; - the first period has no predecessor, so `edge=0` leaves it the bare allowance + `Carrier-growth_limit` — what a carrier adds across its extendable components in a period, + counting each build in the first period it stands in, is at most its allowance plus a share of + what it added the period before; the first period has no predecessor, so `edge=0` leaves it the + bare allowance dims: [carrier, period] where: Carrier_max_growth expression: >- - sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) - - shift(sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier), along=period, offset=1, edge=0) - * Carrier_max_relative_growth + Carrier_additions + - shift(Carrier_additions, along=period, offset=1, edge=0) * Carrier_max_relative_growth <= Carrier_max_growth CVaR_excess: description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index c124d27c..cac7b715 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -102,7 +102,7 @@ "type": "transmission_volume_expansion_limit" } }, - "objective": 7633.908502024291, + "objective": 7633.908502024292, "objective_constant": 0.0, "pypsa": "1.3.0", "rows": { @@ -218,7 +218,7 @@ "type": "operational_limit" } }, - "objective": 10282.833333333332, + "objective": 10282.833333333334, "objective_constant": 0.0, "pypsa": "1.3.0", "rows": { @@ -699,5 +699,41 @@ "Transformer-fix-s-lower": 4, "Transformer-fix-s-upper": 4 } + }, + "rung_21_carrier_growth": { + "columns": { + "Generator-p": 8, + "StorageUnit-p_dispatch": 4, + "StorageUnit-p_nom": 1, + "StorageUnit-p_store": 4, + "StorageUnit-state_of_charge": 4, + "Store-e": 6, + "Store-e_nom": 2, + "Store-p": 6 + }, + "global_constraints": {}, + "objective": 8452.5, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 4, + "Carrier-growth_limit": 2, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "StorageUnit-energy_balance": 4, + "StorageUnit-ext-p_dispatch-lower": 4, + "StorageUnit-ext-p_dispatch-upper": 4, + "StorageUnit-ext-p_nom-lower": 1, + "StorageUnit-ext-p_nom-upper": 1, + "StorageUnit-ext-p_store-lower": 4, + "StorageUnit-ext-p_store-upper": 4, + "StorageUnit-ext-state_of_charge-lower": 4, + "StorageUnit-ext-state_of_charge-upper": 4, + "Store-energy_balance": 6, + "Store-ext-e-lower": 6, + "Store-ext-e-upper": 6, + "Store-ext-e_nom-lower": 2, + "Store-ext-e_nom-upper": 2 + } } } diff --git a/examples/references/pypsa/rung_21_carrier_growth.py b/examples/references/pypsa/rung_21_carrier_growth.py new file mode 100644 index 00000000..505fa064 --- /dev/null +++ b/examples/references/pypsa/rung_21_carrier_growth.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 21: a carrier's growth limit binds its non-generator builds — a storage unit and two stores over two periods.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: cheap solar only at the first snapshot of each period, so battery capacity pays off but its growth is capped.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', 'grid') + n.add('Carrier', 'solar') + n.add('Carrier', 'gas') + n.add('Carrier', 'battery', max_growth=20, max_relative_growth=0.5) + n.add('Generator', 'solar', bus='grid', carrier='solar', p_nom=100, marginal_cost=1, p_max_pu=[1, 0, 1, 0]) + n.add('Generator', 'backup', bus='grid', carrier='gas', p_nom=200, marginal_cost=80) + n.add( + 'StorageUnit', + 'bat20', + bus='grid', + carrier='battery', + p_nom_extendable=True, + p_nom_max=100, + max_hours=4, + capital_cost=50, + build_year=2020, + lifetime=30, + ) + n.add( + 'Store', + 'tank20', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=10, + capital_cost=10, + build_year=2020, + lifetime=30, + ) + n.add( + 'Store', + 'tank30', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=100, + capital_cost=8, + build_year=2030, + lifetime=30, + ) + n.add('Load', 'town', bus='grid', p_set=[40, 60, 40, 80]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 6f1b60f3..0612e5da 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -220,5 +220,11 @@ names: Process_capital_weight: '\mathrm{W}^{z}' Transformer_capital_weight: '\mathrm{W}^{\sigma}' Generator_first_active: '\mathrm{new}' + Link_first_active: '\mathrm{new}^{f}' + StorageUnit_first_active: '\mathrm{new}^{h}' + Store_first_active: '\mathrm{new}^{e}' + Line_first_active: '\mathrm{new}^{s}' + Process_first_active: '\mathrm{new}^{z}' + Transformer_first_active: '\mathrm{new}^{\sigma}' Carrier_max_growth: '\overline{\Delta}' Carrier_max_relative_growth: '\mathrm{r}' From 28926f09975b61f85862fad844bc62c9d5a0d9a4 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 09:59:41 +0200 Subject: [PATCH 09/46] 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/examples/pypsa.md | 227 ++++++++++++++++-- examples/pypsa.yaml | 74 +++++- examples/references/pypsa/references.json | 84 +++++++ .../pypsa/rung_22_transformer_losses.py | 36 +++ .../rung_23_transformer_losses_secants.py | 36 +++ examples/symbols/pypsa.yaml | 4 + tests/fixtures/pypsa_standard_shape.yaml | 11 + 7 files changed, 437 insertions(+), 35 deletions(-) create mode 100644 examples/references/pypsa/rung_22_transformer_losses.py create mode 100644 examples/references/pypsa/rung_23_transformer_losses_secants.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 7556d91b..16a4b122 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1174,13 +1174,13 @@ balance, and the model collapses to the lossless one. | PyPSA | status | note | | --- | --- | --- | -| [`Line-loss`](#variable-domains) | done | absent, and zero in the balance, where lossless | -| [`Line-fix-s-*`, `Line-ext-s-*`](#line-fix-s-lower) | done | the loss counted against the rating | -| [`Bus-nodal_balance`](#bus-nodal_balance) | done | half of each incident line's loss at either end | -| [`Line-loss_upper`](#line-loss_upper) | done | `loss_max` is data prep, see X4 | -| [`Line-loss_tangents-{k}-1`](#line-loss_tangents-k-1) | split | PyPSA names a row per segment; one block over the dimension | -| [`Line-loss_tangents-{k}--1`](#line-loss_tangents-k--1) | split | | -| [`Line-loss_secants-pos`, `Line-loss_secants-neg`](#line-loss_tangents-k-1) | done | the same two blocks in the secant mode; slope, offset and the breakpoint loop are data prep; rung 19 records it | +| [`Line-loss`, `Transformer-loss`](#variable-domains) | done | absent, and zero in the balance, where lossless | +| [`Line-fix-s-*`, `Line-ext-s-*`](#line-fix-s-lower), [`Transformer-fix-s-*`, `Transformer-ext-s-*`](#transformer-fix-s-lower) | done | the loss counted against the rating | +| [`Bus-nodal_balance`](#bus-nodal_balance) | done | half of each incident line's and transformer's loss at either end | +| [`Line-loss_upper`](#line-loss_upper), [`Transformer-loss_upper`](#transformer-loss_upper) | done | `loss_max` is data prep, see X4 | +| [`Line-loss_tangents-{k}-1`](#line-loss_tangents-k-1), [`Transformer-loss_tangents-{k}-1`](#transformer-loss_tangents-k-1) | split | PyPSA names a row per segment; one block over the dimension | +| [`Line-loss_tangents-{k}--1`](#line-loss_tangents-k--1), [`Transformer-loss_tangents-{k}--1`](#transformer-loss_tangents-k--1) | split | | +| [`Line-loss_secants-pos`, `Line-loss_secants-neg`](#line-loss_tangents-k-1), [`Transformer-loss_secants-pos`, `Transformer-loss_secants-neg`](#transformer-loss_tangents-k-1) | done | the same two blocks in the secant mode; slope, offset and the breakpoint loop are data prep; rungs 19 and 23 record it | > ✔ `pypsa 1.3.0` solves this rung's network at objective `10645.295879552297`, 150 rows. @@ -1855,6 +1855,121 @@ def build():
+### Rung 22 — transformer losses + +PyPSA applies the loss of rung 13 to every passive branch, so a transformer +dissipates a loss as a line does: its own loss variable, the loss counted +against its rating, its cap and its fan of cuts, and half of it at either end +in the balance. The loss curve is `r_pu_eff * p**2`, where a transformer's +`r_pu_eff` is its resistance over its given `s_nom`, times its tap ratio +(`power_flow.py:815`). The given `s_nom` sets it also for an extendable +transformer, whose build does not move the curve. Here the loss of the +extendable transformer is counted against its rating, so it builds more than +the flow it carries. + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `10643.477135410736`, 174 rows. + +
+The network, as PyPSA code + +`rung_22_transformer_losses.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 22: transformer losses in tangent form — a loss per transformer, as per line.""" + +from __future__ import annotations + +import spine + +OPTIMIZE = {'transmission_losses': {'mode': 'tangents', 'segments': 3}} + + +def build(): + """The spine plus a triangle of one 110 kV line and two transformers, one extendable and off-nominal tap, per-unit resistances a real transformer has, so its loss stays a few percent of the flow.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c'], v_nom=110) + n.add('Generator', 'hydro22', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel22', bus='b', p_nom=80, marginal_cost=50) + n.add('Line', 'ab22', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) + n.add('Transformer', 'bc22', bus0='b', bus1='c', x=0.1, r=0.03, s_nom=60) + n.add( + 'Transformer', + 'ca22', + bus0='c', + bus1='a', + x=0.12, + r=0.02, + s_nom=40, + s_nom_extendable=True, + s_nom_max=90, + capital_cost=4, + tap_ratio=1.05, + ) + n.add('Load', 'town22', bus='c', p_set=[35, 55, 15, 45]) + return n +``` + +
+ + +The same triangle solved in the secant mode records the identical loss rows, +its cuts placed by PyPSA's tolerance loop. + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `10821.999155213578`, 142 rows. + +
+The network, as PyPSA code + +`rung_23_transformer_losses_secants.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 23: transformer losses in secant form — the same loss per transformer, its cuts placed by PyPSA's tolerance loop.""" + +from __future__ import annotations + +import spine + +OPTIMIZE = {'transmission_losses': {'mode': 'secants', 'atol': 1, 'rtol': 0.1, 'max_segments': 20}} + + +def build(): + """Rung 22's triangle, unchanged, so the two modes differ only in the cuts.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c'], v_nom=110) + n.add('Generator', 'hydro23', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel23', bus='b', p_nom=80, marginal_cost=50) + n.add('Line', 'ab23', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) + n.add('Transformer', 'bc23', bus0='b', bus1='c', x=0.1, r=0.03, s_nom=60) + n.add( + 'Transformer', + 'ca23', + bus0='c', + bus1='a', + x=0.12, + r=0.02, + s_nom=40, + s_nom_extendable=True, + s_nom_max=90, + capital_cost=4, + tap_ratio=1.05, + ) + n.add('Load', 'town23', bus='c', p_set=[35, 55, 15, 45]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -1868,7 +1983,7 @@ data prep, or harness — is one open question. Line numbers are pinned pypsa | `ValueError`, `constraints.py:1557` | load on a bus with nothing attached | row not built, unserved | X2 | | `ValueError`, `optimize.py:436` | no component carries a cost | feasibility problem | X3 | | `NotImplementedError`, `global_constraints.py:457` | depletion with period weightings `!= 1` | out | | -| `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` | X4 | +| `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` and `Transformer_loss_max` | X4 | | `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | Duals and solutions are read back by the harness on the lpspec side: @@ -1898,7 +2013,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | | $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | -| $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a line's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | +| $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a passive branch's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | | $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | | $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | @@ -2059,6 +2174,9 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{Transformer\_phase\_shift\_min}`$ | `Transformer_phase_shift_min` over $`\mathcal{M}`$ — the least a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_min`; where it is below `phase_shift_max` the shift is a decision, otherwise the transformer keeps its fixed `phase_shift` | | $`\mathrm{Transformer\_phase\_shift\_max}`$ | `Transformer_phase_shift_max` over $`\mathcal{M}`$ — the most a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_max`; equal to `phase_shift_min` for a fixed transformer | | $`\mathrm{Transformer\_phase\_shift\_cycle\_weight}`$ | `Transformer_phase_shift_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the cycle sign for a varying transformer's phase shift, times π/180 so a shift in degrees enters the cycle sum in radians — data prep; zero for a fixed transformer or one in no cycle | +| $`\overline{\ell}^{\sigma}`$ | `Transformer_loss_max` over $`\mathcal{T} \times \mathcal{M}`$ — the loss at a transformer's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, its `r_pu_eff` the resistance over the given `s_nom` times the tap ratio, data prep | +| $`\mathrm{a}^{\sigma}`$ | `Transformer_loss_slope` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — the slope of a cut to a transformer's loss curve — a tangent's `2 * r_pu_eff * p_k`, a secant's `r_pu_eff * (p_k + p_k+1)`, as a line's, over the transformer's own `r_pu_eff` and rating, data prep | +| $`\mathrm{b}^{\sigma}`$ | `Transformer_loss_offset` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | | $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{I}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | | $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\mathcal{I}`$ — which way the row binds — `<=`, `>=` or `==` | | $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, a non-extendable build — is folded in here by data prep | @@ -2100,6 +2218,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | | $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | +| $`\ell^{\sigma}`$ | `Transformer_loss` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-loss` — what a transformer dissipates carrying its flow, as a line does; absent, and zero in the balance, where the network is lossless | | $`\mathit{Transformer\_phase\_shift}`$ | `Transformer_phase_shift` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-phase_shift` — a phase-shifting transformer's voltage angle shift in degrees, chosen per snapshot to redistribute the flows around its cycles without moving active power; absent, and zero in the cycle sum, where the shift is fixed | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | @@ -3263,14 +3382,14 @@ Line_loss_tangents_reverse: ```yaml Transformer_fix_s_lower: - description: "`Transformer-fix-s-lower` — a fixed transformer carries at least the negative of its rating" + description: "`Transformer-fix-s-lower` — a fixed transformer carries at least the negative of its rating, the loss counted against it" dims: [scenario, snapshot, transformer] where: not Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s - Transformer_loss >= -Transformer_s_max_pu * Transformer_s_nom ``` ```math -\sigma_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-fix-s-upper` @@ -3279,14 +3398,14 @@ Transformer_fix_s_lower: ```yaml Transformer_fix_s_upper: - description: "`Transformer-fix-s-upper` — a fixed transformer carries at most its rating" + description: "`Transformer-fix-s-upper` — a fixed transformer carries at most its rating, the loss included" dims: [scenario, snapshot, transformer] where: not Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s + Transformer_loss <= Transformer_s_max_pu * Transformer_s_nom ``` ```math -\sigma_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-ext-s-lower` @@ -3295,14 +3414,14 @@ Transformer_fix_s_upper: ```yaml Transformer_ext_s_lower: - description: "`Transformer-ext-s-lower` — an extendable transformer carries at least the negative of its rating of the chosen build" + description: "`Transformer-ext-s-lower` — an extendable transformer carries at least the negative of its rating of the chosen build, the loss counted against it" dims: [scenario, snapshot, transformer] where: Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s - Transformer_loss >= -Transformer_s_max_pu * Transformer_s_nom_ext ``` ```math -\sigma_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-ext-s-upper` @@ -3311,14 +3430,14 @@ Transformer_ext_s_lower: ```yaml Transformer_ext_s_upper: - description: "`Transformer-ext-s-upper` — an extendable transformer carries at most its rating of the chosen build" + description: "`Transformer-ext-s-upper` — an extendable transformer carries at most its rating of the chosen build, the loss included" dims: [scenario, snapshot, transformer] where: Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s + Transformer_loss <= Transformer_s_max_pu * Transformer_s_nom_ext ``` ```math -\sigma_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-ext-s_nom-lower` @@ -3385,6 +3504,59 @@ Transformer_s_set: \sigma_{\xi,t,m} = \sigma^{\mathrm{set}}_{t,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \sigma^{\mathrm{set}}_{t,m} \text{ is defined} \wedge \mathrm{on}^{\sigma}_{t,m} ``` +### `Transformer-loss_upper` + +`Transformer_loss_upper` + +```yaml +Transformer_loss_upper: + description: "`Transformer-loss_upper` — a transformer dissipates at most the loss at its rating" + dims: [scenario, snapshot, transformer] + where: transmission_losses AND Transformer_active + expression: Transformer_loss <= Transformer_loss_max +``` + +```math +\ell^{\sigma}_{\xi,t,m} \le \overline{\ell}^{\sigma}_{t,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + +### `Transformer-loss_tangents-{k}-1` + +`Transformer_loss_tangents_forward` + +```yaml +Transformer_loss_tangents_forward: + description: >- + `Transformer-loss_tangents-{k}-1`, `Transformer-loss_secants-pos` — the + loss sits above every cut to its curve for flow one way, as a line's + does, over the segment dimension + dims: [scenario, snapshot, transformer, segment] + where: transmission_losses AND Transformer_active + expression: Transformer_loss + Transformer_loss_slope * Transformer_s >= Transformer_loss_offset +``` + +```math +\ell^{\sigma}_{\xi,t,m} + \mathrm{a}^{\sigma}_{t,m,b} \cdot \sigma_{\xi,t,m} \ge \mathrm{b}^{\sigma}_{t,m,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + +### `Transformer-loss_tangents-{k}--1` + +`Transformer_loss_tangents_reverse` + +```yaml +Transformer_loss_tangents_reverse: + description: >- + `Transformer-loss_tangents-{k}--1`, `Transformer-loss_secants-neg` — the + same fan mirrored, the loss depending on the flow's magnitude + dims: [scenario, snapshot, transformer, segment] + where: transmission_losses AND Transformer_active + expression: Transformer_loss - Transformer_loss_slope * Transformer_s >= Transformer_loss_offset +``` + +```math +\ell^{\sigma}_{\xi,t,m} - \mathrm{a}^{\sigma}_{t,m,b} \cdot \sigma_{\xi,t,m} \ge \mathrm{b}^{\sigma}_{t,m,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + ### `Kirchhoff-Voltage-Law` `Kirchhoff_Voltage_Law` @@ -4177,7 +4349,8 @@ Bus_nodal_balance: them after losses and any delay at every port they deliver to, each process port drawing or delivering at its own rate and each passive branch carrying its flow, meets the load there, less half of every incident - line's loss — PyPSA dissipates a branch's loss half at either end. + line's and transformer's loss — PyPSA dissipates a branch's loss half at + either end. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] @@ -4194,11 +4367,13 @@ Bus_nodal_balance: - 0.5 * sum(Line_loss, by=Line_bus1, over=line, into=bus) - sum(Transformer_s, by=Transformer_bus0, over=transformer, into=bus) + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) + - 0.5 * sum(Transformer_loss, by=Transformer_bus0, over=transformer, into=bus) + - 0.5 * sum(Transformer_loss, by=Transformer_bus1, over=transformer, into=bus) == sum(Load_p_set, by=Load_bus, over=load, into=bus) ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} ``` ### `Carrier-growth_limit` @@ -4701,6 +4876,12 @@ s_{\xi,t,k} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \sigma_{\xi,t,m} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{on}^{\sigma}_{t,m} ``` +**`Transformer_loss`** + +```math +\ell^{\sigma}_{\xi,t,m} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} +``` + **`Transformer_phase_shift`** ```math diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 594ff0b1..b5992207 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -51,9 +51,9 @@ dimensions: description: independent cycles of the passive network graph — the cycle basis, data prep segment: description: >- - the cuts a line's loss curve is held above — PyPSA's tangents, as many as - its `segments` count, or its secants, as many as its tolerance loop - places; none in a lossless run + the cuts a passive branch's loss curve is held above — PyPSA's tangents, + as many as its `segments` count, or its secants, as many as its tolerance + loop places; none in a lossless run dtype: int global_constraint: description: PyPSA's `GlobalConstraint` rows, one label per declared limit @@ -607,6 +607,23 @@ parameters: shift in degrees enters the cycle sum in radians — data prep; zero for a fixed transformer or one in no cycle dims: [transformer, cycle] + Transformer_loss_max: + description: >- + the loss at a transformer's rating — PyPSA's `r_pu_eff * (s_max_pu * + s_nom_max)**2`, its `r_pu_eff` the resistance over the given `s_nom` times + the tap ratio, data prep + dims: [snapshot, transformer] + Transformer_loss_slope: + description: >- + the slope of a cut to a transformer's loss curve — a tangent's + `2 * r_pu_eff * p_k`, a secant's `r_pu_eff * (p_k + p_k+1)`, as a line's, + over the transformer's own `r_pu_eff` and rating, data prep + dims: [snapshot, transformer, segment] + Transformer_loss_offset: + description: >- + where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, + a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep + dims: [snapshot, transformer, segment] GlobalConstraint_type: description: >- which formula the row takes — `primary_energy`, `operational_limit`, @@ -879,6 +896,16 @@ variables: `Transformer_bus1`, lossless dims: [scenario, snapshot, transformer] where: Transformer_active + Transformer_loss: + description: >- + `Transformer-loss` — what a transformer dissipates carrying its flow, as + a line does; absent, and zero in the balance, where the network is + lossless + dims: [scenario, snapshot, transformer] + where: transmission_losses AND Transformer_active + absence: zero + bounds: + lower: 0 Transformer_phase_shift: description: >- `Transformer-phase_shift` — a phase-shifting transformer's voltage angle @@ -1524,25 +1551,25 @@ constraints: where: transmission_losses AND Line_active expression: Line_loss - Line_loss_slope * Line_s >= Line_loss_offset Transformer_fix_s_lower: - description: "`Transformer-fix-s-lower` — a fixed transformer carries at least the negative of its rating" + description: "`Transformer-fix-s-lower` — a fixed transformer carries at least the negative of its rating, the loss counted against it" dims: [scenario, snapshot, transformer] where: not Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s - Transformer_loss >= -Transformer_s_max_pu * Transformer_s_nom Transformer_fix_s_upper: - description: "`Transformer-fix-s-upper` — a fixed transformer carries at most its rating" + description: "`Transformer-fix-s-upper` — a fixed transformer carries at most its rating, the loss included" dims: [scenario, snapshot, transformer] where: not Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s + Transformer_loss <= Transformer_s_max_pu * Transformer_s_nom Transformer_ext_s_lower: - description: "`Transformer-ext-s-lower` — an extendable transformer carries at least the negative of its rating of the chosen build" + description: "`Transformer-ext-s-lower` — an extendable transformer carries at least the negative of its rating of the chosen build, the loss counted against it" dims: [scenario, snapshot, transformer] where: Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s - Transformer_loss >= -Transformer_s_max_pu * Transformer_s_nom_ext Transformer_ext_s_upper: - description: "`Transformer-ext-s-upper` — an extendable transformer carries at most its rating of the chosen build" + description: "`Transformer-ext-s-upper` — an extendable transformer carries at most its rating of the chosen build, the loss included" dims: [scenario, snapshot, transformer] where: Transformer_s_nom_extendable AND Transformer_active - expression: Transformer_s <= Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s + Transformer_loss <= Transformer_s_max_pu * Transformer_s_nom_ext Transformer_ext_s_nom_lower: description: "`Transformer-ext-s_nom-lower` — the chosen build is at least its floor" dims: [transformer] @@ -1563,6 +1590,26 @@ constraints: dims: [scenario, snapshot, transformer] where: Transformer_s_set AND Transformer_active expression: Transformer_s == Transformer_s_set + Transformer_loss_upper: + description: "`Transformer-loss_upper` — a transformer dissipates at most the loss at its rating" + dims: [scenario, snapshot, transformer] + where: transmission_losses AND Transformer_active + expression: Transformer_loss <= Transformer_loss_max + Transformer_loss_tangents_forward: + description: >- + `Transformer-loss_tangents-{k}-1`, `Transformer-loss_secants-pos` — the + loss sits above every cut to its curve for flow one way, as a line's + does, over the segment dimension + dims: [scenario, snapshot, transformer, segment] + where: transmission_losses AND Transformer_active + expression: Transformer_loss + Transformer_loss_slope * Transformer_s >= Transformer_loss_offset + Transformer_loss_tangents_reverse: + description: >- + `Transformer-loss_tangents-{k}--1`, `Transformer-loss_secants-neg` — the + same fan mirrored, the loss depending on the flow's magnitude + dims: [scenario, snapshot, transformer, segment] + where: transmission_losses AND Transformer_active + expression: Transformer_loss - Transformer_loss_slope * Transformer_s >= Transformer_loss_offset Kirchhoff_Voltage_Law: description: >- `Kirchhoff-Voltage-Law` — around every independent cycle the @@ -1844,7 +1891,8 @@ constraints: them after losses and any delay at every port they deliver to, each process port drawing or delivering at its own rate and each passive branch carrying its flow, meets the load there, less half of every incident - line's loss — PyPSA dissipates a branch's loss half at either end. + line's and transformer's loss — PyPSA dissipates a branch's loss half at + either end. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] @@ -1861,6 +1909,8 @@ constraints: - 0.5 * sum(Line_loss, by=Line_bus1, over=line, into=bus) - sum(Transformer_s, by=Transformer_bus0, over=transformer, into=bus) + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) + - 0.5 * sum(Transformer_loss, by=Transformer_bus0, over=transformer, into=bus) + - 0.5 * sum(Transformer_loss, by=Transformer_bus1, over=transformer, into=bus) == sum(Load_p_set, by=Load_bus, over=load, into=bus) Carrier_growth_limit: description: >- diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index cac7b715..47fe75e2 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -735,5 +735,89 @@ "Store-ext-e_nom-lower": 2, "Store-ext-e_nom-upper": 2 } + }, + "rung_22_transformer_losses": { + "columns": { + "Generator-p": 16, + "Line-loss": 4, + "Line-s": 4, + "Link-p": 4, + "Transformer-loss": 8, + "Transformer-s": 8, + "Transformer-s_nom": 1, + "objective_constant": 1 + }, + "global_constraints": {}, + "objective": 10643.477135410736, + "objective_constant": 160.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 20, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Kirchhoff-Voltage-Law": 4, + "Line-fix-s-lower": 4, + "Line-fix-s-upper": 4, + "Line-loss_tangents-1--1": 4, + "Line-loss_tangents-1-1": 4, + "Line-loss_tangents-2--1": 4, + "Line-loss_tangents-2-1": 4, + "Line-loss_tangents-3--1": 4, + "Line-loss_tangents-3-1": 4, + "Line-loss_upper": 4, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Transformer-ext-s-lower": 4, + "Transformer-ext-s-upper": 4, + "Transformer-ext-s_nom-lower": 1, + "Transformer-ext-s_nom-upper": 1, + "Transformer-fix-s-lower": 4, + "Transformer-fix-s-upper": 4, + "Transformer-loss_tangents-1--1": 8, + "Transformer-loss_tangents-1-1": 8, + "Transformer-loss_tangents-2--1": 8, + "Transformer-loss_tangents-2-1": 8, + "Transformer-loss_tangents-3--1": 8, + "Transformer-loss_tangents-3-1": 8, + "Transformer-loss_upper": 8 + } + }, + "rung_23_transformer_losses_secants": { + "columns": { + "Generator-p": 16, + "Line-loss": 4, + "Line-s": 4, + "Link-p": 4, + "Transformer-loss": 8, + "Transformer-s": 8, + "Transformer-s_nom": 1, + "objective_constant": 1 + }, + "global_constraints": {}, + "objective": 10821.999155213578, + "objective_constant": 160.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 20, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Kirchhoff-Voltage-Law": 4, + "Line-fix-s-lower": 4, + "Line-fix-s-upper": 4, + "Line-loss_secants-neg": 4, + "Line-loss_secants-pos": 4, + "Line-loss_upper": 4, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Transformer-ext-s-lower": 4, + "Transformer-ext-s-upper": 4, + "Transformer-ext-s_nom-lower": 1, + "Transformer-ext-s_nom-upper": 1, + "Transformer-fix-s-lower": 4, + "Transformer-fix-s-upper": 4, + "Transformer-loss_secants-neg": 16, + "Transformer-loss_secants-pos": 16, + "Transformer-loss_upper": 8 + } } } diff --git a/examples/references/pypsa/rung_22_transformer_losses.py b/examples/references/pypsa/rung_22_transformer_losses.py new file mode 100644 index 00000000..ac33c34a --- /dev/null +++ b/examples/references/pypsa/rung_22_transformer_losses.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 22: transformer losses in tangent form — a loss per transformer, as per line.""" + +from __future__ import annotations + +import spine + +OPTIMIZE = {'transmission_losses': {'mode': 'tangents', 'segments': 3}} + + +def build(): + """The spine plus a triangle of one 110 kV line and two transformers, one extendable and off-nominal tap, per-unit resistances a real transformer has, so its loss stays a few percent of the flow.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c'], v_nom=110) + n.add('Generator', 'hydro22', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel22', bus='b', p_nom=80, marginal_cost=50) + n.add('Line', 'ab22', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) + n.add('Transformer', 'bc22', bus0='b', bus1='c', x=0.1, r=0.03, s_nom=60) + n.add( + 'Transformer', + 'ca22', + bus0='c', + bus1='a', + x=0.12, + r=0.02, + s_nom=40, + s_nom_extendable=True, + s_nom_max=90, + capital_cost=4, + tap_ratio=1.05, + ) + n.add('Load', 'town22', bus='c', p_set=[35, 55, 15, 45]) + return n diff --git a/examples/references/pypsa/rung_23_transformer_losses_secants.py b/examples/references/pypsa/rung_23_transformer_losses_secants.py new file mode 100644 index 00000000..7421dc83 --- /dev/null +++ b/examples/references/pypsa/rung_23_transformer_losses_secants.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 23: transformer losses in secant form — the same loss per transformer, its cuts placed by PyPSA's tolerance loop.""" + +from __future__ import annotations + +import spine + +OPTIMIZE = {'transmission_losses': {'mode': 'secants', 'atol': 1, 'rtol': 0.1, 'max_segments': 20}} + + +def build(): + """Rung 22's triangle, unchanged, so the two modes differ only in the cuts.""" + n = spine.build() + n.add('Bus', ['a', 'b', 'c'], v_nom=110) + n.add('Generator', 'hydro23', bus='a', p_nom=80, marginal_cost=10) + n.add('Generator', 'diesel23', bus='b', p_nom=80, marginal_cost=50) + n.add('Line', 'ab23', bus0='a', bus1='b', carrier='AC', x=30, r=6, s_nom=60) + n.add('Transformer', 'bc23', bus0='b', bus1='c', x=0.1, r=0.03, s_nom=60) + n.add( + 'Transformer', + 'ca23', + bus0='c', + bus1='a', + x=0.12, + r=0.02, + s_nom=40, + s_nom_extendable=True, + s_nom_max=90, + capital_cost=4, + tap_ratio=1.05, + ) + n.add('Load', 'town23', bus='c', p_set=[35, 55, 15, 45]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 0612e5da..16aca2d0 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -144,6 +144,10 @@ names: Transformer_s_set: '\sigma^{\mathrm{set}}' Transformer_cycle_weight: '\mathrm{x}^{\sigma}' Transformer_phase_shift_weight: '\vartheta' + Transformer_loss: '\ell^{\sigma}' + Transformer_loss_max: '\overline{\ell}^{\sigma}' + Transformer_loss_slope: '\mathrm{a}^{\sigma}' + Transformer_loss_offset: '\mathrm{b}^{\sigma}' GlobalConstraint_type: '\mathrm{type}' GlobalConstraint_sense: '\mathrm{sense}' GlobalConstraint_constant: '\mathrm{K}' diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index 7e66c9bd..abf708ac 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -657,6 +657,17 @@ parameters: Transformer_cycle_weight: - transformer - cycle + Transformer_loss_max: + - snapshot + - transformer + Transformer_loss_offset: + - snapshot + - transformer + - segment + Transformer_loss_slope: + - snapshot + - transformer + - segment Transformer_phase_shift_cycle_weight: - transformer - cycle From cdcc0415199cf5033498993a2b1c0280529bc258 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 10:01:37 +0200 Subject: [PATCH 10/46] 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/examples/pypsa.md | 79 ++++++++++++++++++- examples/pypsa.yaml | 19 ++++- examples/references/pypsa/references.json | 29 +++++++ .../pypsa/rung_24_must_stay_down.py | 29 +++++++ examples/symbols/pypsa.yaml | 1 + tests/fixtures/pypsa_standard_shape.yaml | 6 ++ 6 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 examples/references/pypsa/rung_24_must_stay_down.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 16a4b122..d734d66f 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -601,7 +601,8 @@ def build(): | [`{c}-*-p-fixed-upper`](#generator-status-p-fixed-upper) | done | status, start and stop each at most one, as explicit rows | | [`{c}-com-transition-start-up/shut-down`](#generator-com-transition-start-up) | done | the state carried into a snapshot is a cased quantity, so the first snapshot needs no block of its own | | [`{c}-com-up-time`, `-down-time`](#generator-com-up-time) | done | `sum_back(window=min_up_time)` | -| [`{c}-com-status-*-must_stay_up`](#generator-com-status-min_up_time_must_stay_up) | done | the window is a prep mask — `position()` takes a literal | +| [`{c}-com-status-min_up_time_must_stay_up`](#generator-com-status-min_up_time_must_stay_up) | done | the window is a prep mask — `position()` takes a literal | +| [`{c}-com-status-min_down_time_must_stay_up`](#generator-com-status-min_down_time_must_stay_up) | done | the same prep mask over the down time brought in, status zero; PyPSA's name says `_must_stay_up`; rung 24 records it | | [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | | [`{c}-com-p-before/-current/-partly-*`](pypsa_linearized_uc.md) | done | rung 12, a file of its own | @@ -1970,6 +1971,58 @@ def build(): +### Rung 24 — must stay down + +A committable unit that stopped `down_time_before` snapshots before the horizon +stays off until its `min_down_time` has passed. PyPSA fixes its status to zero +in the first `min_down_time - down_time_before` snapshots. Here the cheapest +unit in the network brought one snapshot of a three-snapshot down time into +the horizon, so it stays off for two snapshots and the dearer coal unit serves +the load. + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `9007.5`, 65 rows. + +
+The network, as PyPSA code + +`rung_24_must_stay_down.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 24: must stay down — a committable unit still serving the down time it brought in stays off.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a cheap committable unit that stopped one snapshot before the horizon and must stay off for three.""" + n = spine.build() + n.add( + 'Generator', + 'warm', + bus='north', + committable=True, + p_nom=50, + marginal_cost=5, + p_min_pu=0.2, + min_down_time=3, + up_time_before=0, + down_time_before=1, + start_up_cost=20, + ) + n.add('Load', 'swing24', bus='north', p_set=[25, 45, 45, 10]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2038,6 +2091,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{DT}`$ | `Generator_min_down_time` over $`\mathcal{G}`$ — least snapshots a unit stays off once stopped | | $`\mathrm{u}^{0}`$ | `Generator_status_initial` over $`\mathcal{G}`$ — one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | | $`\mathrm{hold}`$ | `Generator_must_stay_up` over $`\mathcal{T} \times \mathcal{G}`$ — true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | +| $`\mathrm{rest}`$ | `Generator_must_stay_down` over $`\mathcal{T} \times \mathcal{G}`$ — true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\mathcal{G}`$ — cost of one start | | $`\mathrm{c}^{\mathrm{dn}}`$ | `Generator_shut_down_cost` over $`\mathcal{G}`$ — cost of one stop | | $`\mathrm{c}^{\mathrm{on}}`$ | `Generator_stand_by_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one snapshot spent on | @@ -2839,7 +2893,10 @@ Generator_com_up_time: ```yaml Generator_com_down_time: - description: "`Generator-com-down-time` — a unit stopped within its own minimum down time is still off" + description: >- + `Generator-com-down-time` — a unit stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 AND Generator_active expression: sum_back(Generator_shut_down, along=snapshot, window=Generator_min_down_time) <= 1 - Generator_status @@ -2865,6 +2922,24 @@ Generator_com_status_must_stay_up: u_{\xi,t,g} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{hold}_{t,g} \wedge \mathrm{on}_{t,g} ``` +### `Generator-com-status-min_down_time_must_stay_up` + +`Generator_com_status_must_stay_down` + +```yaml +Generator_com_status_must_stay_down: + description: >- + `Generator-com-status-min_down_time_must_stay_up` — a unit still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_must_stay_down AND Generator_active + expression: Generator_status == 0 +``` + +```math +u_{\xi,t,g} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{rest}_{t,g} \wedge \mathrm{on}_{t,g} +``` + ### `Generator-p-ramp_limit_up-run-bigM` `Generator_p_ramp_limit_up_run_big_m` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index b5992207..bd1fe383 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -121,6 +121,13 @@ parameters: parameter dims: [snapshot, generator] dtype: bool + Generator_must_stay_down: + description: >- + true while the down time a unit brought into the horizon still binds — + PyPSA's `min_down_time - down_time_before` snapshots, where + `down_time_before > 0`, data prep for the same reason + dims: [snapshot, generator] + dtype: bool Generator_start_up_cost: description: cost of one start dims: [generator] @@ -1338,7 +1345,10 @@ constraints: where: Generator_committable AND Generator_min_up_time > 0 AND position(snapshot) > 0 AND Generator_active expression: sum_back(Generator_start_up, along=snapshot, window=Generator_min_up_time) <= Generator_status Generator_com_down_time: - description: "`Generator-com-down-time` — a unit stopped within its own minimum down time is still off" + description: >- + `Generator-com-down-time` — a unit stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 AND Generator_active expression: sum_back(Generator_shut_down, along=snapshot, window=Generator_min_down_time) <= 1 - Generator_status @@ -1347,6 +1357,13 @@ constraints: dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_must_stay_up AND Generator_active expression: Generator_status == 1 + Generator_com_status_must_stay_down: + description: >- + `Generator-com-status-min_down_time_must_stay_up` — a unit still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [scenario, snapshot, generator] + where: Generator_committable AND Generator_must_stay_down AND Generator_active + expression: Generator_status == 0 Generator_p_ramp_limit_up_run_big_m: description: >- `Generator-p-ramp_limit_up-run-bigM` — a committed extendable unit diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 47fe75e2..c44f97ff 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -819,5 +819,34 @@ "Transformer-loss_secants-pos": 16, "Transformer-loss_upper": 8 } + }, + "rung_24_must_stay_down": { + "columns": { + "Generator-p": 12, + "Generator-shut_down": 4, + "Generator-start_up": 4, + "Generator-status": 4, + "Link-p": 4 + }, + "global_constraints": {}, + "objective": 9007.5, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-com-down-time": 3, + "Generator-com-p-lower": 4, + "Generator-com-p-upper": 4, + "Generator-com-status-min_down_time_must_stay_up": 2, + "Generator-com-transition-shut-down": 4, + "Generator-com-transition-start-up": 4, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Generator-shut_down-p-fixed-upper": 4, + "Generator-start_up-p-fixed-upper": 4, + "Generator-status-p-fixed-upper": 4, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_24_must_stay_down.py b/examples/references/pypsa/rung_24_must_stay_down.py new file mode 100644 index 00000000..47295e34 --- /dev/null +++ b/examples/references/pypsa/rung_24_must_stay_down.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 24: must stay down — a committable unit still serving the down time it brought in stays off.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a cheap committable unit that stopped one snapshot before the horizon and must stay off for three.""" + n = spine.build() + n.add( + 'Generator', + 'warm', + bus='north', + committable=True, + p_nom=50, + marginal_cost=5, + p_min_pu=0.2, + min_down_time=3, + up_time_before=0, + down_time_before=1, + start_up_cost=20, + ) + n.add('Load', 'swing24', bus='north', p_set=[25, 45, 45, 10]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 16aca2d0..e46daf0d 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -47,6 +47,7 @@ names: Generator_ramp_up_allowance: '\Delta^{+}' Generator_ramp_down_allowance: '\Delta^{-}' Generator_must_stay_up: '\mathrm{hold}' + Generator_must_stay_down: '\mathrm{rest}' Generator_start_up_cost: '\mathrm{c}^{\mathrm{up}}' Generator_shut_down_cost: '\mathrm{c}^{\mathrm{dn}}' Generator_stand_by_cost: '\mathrm{c}^{\mathrm{on}}' diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index abf708ac..c5acc4ce 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -37,6 +37,9 @@ constraints: Generator_com_p_upper: - snapshot - generator + Generator_com_status_must_stay_down: + - snapshot + - generator Generator_com_status_must_stay_up: - snapshot - generator @@ -356,6 +359,9 @@ parameters: - generator Generator_modules_installed: - generator + Generator_must_stay_down: + - snapshot + - generator Generator_must_stay_up: - snapshot - generator From a4baa2cfe1259f71550a673a18b4209db9b20373 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 10:19:25 +0200 Subject: [PATCH 11/46] 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/examples/pypsa.md | 777 +++++++++++++++++- examples/pypsa.yaml | 370 ++++++++- examples/references/pypsa/references.json | 51 ++ .../pypsa/rung_25_committable_link.py | 89 ++ examples/symbols/pypsa.yaml | 23 + tests/fixtures/pypsa_standard_shape.yaml | 122 +++ tests/test_pypsa_collapses.py | 1 + 7 files changed, 1401 insertions(+), 32 deletions(-) create mode 100644 examples/references/pypsa/rung_25_committable_link.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index d734d66f..5e212b5d 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -596,7 +596,7 @@ def build(): | PyPSA | status | note | | -------------------------------------------- | ------ | ------------------------------------------------------------- | -| [`{c}-status`, `-start_up`, `-shut_down`](#variable-domains) | done | Generator; a committable link is not taken up here | +| [`{c}-status`, `-start_up`, `-shut_down`](#variable-domains) | done | Generator; Link in rung 25 | | [`{c}-com-p-lower/upper`](#generator-com-p-lower) | done | | | [`{c}-*-p-fixed-upper`](#generator-status-p-fixed-upper) | done | status, start and stop each at most one, as explicit rows | | [`{c}-com-transition-start-up/shut-down`](#generator-com-transition-start-up) | done | the state carried into a snapshot is a cased quantity, so the first snapshot needs no block of its own | @@ -2023,6 +2023,134 @@ def build(): +### Rung 25 — committable links + +A committable link carries the generator's whole unit commitment over its flow. +PyPSA builds the same status, transition, up time, down time, must-stay, +big-M, modular and ramp rows for a `Link` as for a `Generator`, and prices its +starts, stops and stand-by snapshots the same way. Here an east bus is served +only by committable links. `hvdc` brought one snapshot of a three-snapshot up +time into the horizon, so it stays on for two snapshots although a cheaper link +could carry the load. `cold_tie` brought one snapshot of a three-snapshot down +time, so it stays off for two snapshots. Its own two-snapshot up time would +then hold it on into the last snapshot, where the load is below its minimum, so +it does not start at all. The other links are committable builds that are +extendable, modular, or both. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Link-status`, `-start_up`, `-shut_down`, `-n_mod`](#variable-domains) | done | as the generator's, rung 7 and 8 | +| [`Link-com-p-*`, `-com-mod-p-*`, `-com-ext-p-*`](#link-com-p-lower) | done | a committable link leaves the `Link-fix-p-*` and `Link-ext-p-*` rows, as a generator does | +| [`Link-*-p-fixed-upper`, `-*-p_nom-variable-upper`](#link-status-p-fixed-upper) | done | | +| [`Link-com-transition-*`, `-com-up-time`, `-com-down-time`](#link-com-transition-start-up) | done | | +| [`Link-com-status-min_up_time_must_stay_up`, `-min_down_time_must_stay_up`](#link-com-status-min_up_time_must_stay_up) | done | prep masks, as the generator's | +| [`Link-p-ramp_limit_*`, `-*-bigM`](#link-p-ramp_limit_up) | done | the generator's cased allowance and big-M rows over flow | +| [`Link-p_nom_modularity`](#link-p_nom_modularity) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `14013.0`, 235 rows. + +
+The network, as PyPSA code + +`rung_25_committable_link.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 25: committable links — a link held on by the up time it brought in, one held off by its down time, one kept on by its own up time, and committable builds that are extendable, modular or both.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus that only committable links serve.""" + n = spine.build() + n.add('Bus', 'east') + n.add( + 'Link', + 'hvdc', + bus0='north', + bus1='east', + committable=True, + p_nom=60, + p_min_pu=0.3, + marginal_cost=8, + min_up_time=3, + min_down_time=2, + up_time_before=1, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ramp_limit_start_up=0.6, + ramp_limit_shut_down=0.6, + start_up_cost=100, + shut_down_cost=50, + stand_by_cost=5, + ) + n.add( + 'Link', + 'cold_tie', + bus0='north', + bus1='east', + committable=True, + p_nom=40, + p_min_pu=0.2, + min_up_time=2, + min_down_time=3, + up_time_before=0, + down_time_before=1, + start_up_cost=20, + ) + n.add( + 'Link', + 'ext_tie', + bus0='north', + bus1='east', + committable=True, + p_nom_extendable=True, + p_nom_max=30, + capital_cost=5, + p_min_pu=0.2, + marginal_cost=2, + up_time_before=0, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ) + n.add( + 'Link', + 'mod_tie', + bus0='south', + bus1='east', + committable=True, + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=40, + capital_cost=3, + p_min_pu=0.5, + ) + n.add( + 'Link', + 'mod_fix', + bus0='north', + bus1='east', + committable=True, + p_nom=20, + p_nom_mod=10, + p_min_pu=0.5, + marginal_cost=1, + ) + n.add('Load', 'east_load', bus='east', p_set=[20, 70, 60, 5]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2110,6 +2238,21 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the horizon's end — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at the first snapshots is lost | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | | $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | +| $`\mathrm{com}^{f}`$ | `Link_committable` over $`\mathcal{L}`$ — whether flow is gated by an on/off status decision | +| $`\mathrm{ru}^{f,\mathrm{up}}`$ | `Link_ramp_limit_start_up` over $`\mathcal{L}`$ — most flow in the snapshot a link starts, per unit of nominal power | +| $`\mathrm{rd}^{f,\mathrm{dn}}`$ | `Link_ramp_limit_shut_down` over $`\mathcal{L}`$ — most flow in the snapshot before a link stops, per unit of nominal power | +| $`\mathrm{UT}^{f}`$ | `Link_min_up_time` over $`\mathcal{L}`$ — least snapshots a link stays on once started | +| $`\mathrm{DT}^{f}`$ | `Link_min_down_time` over $`\mathcal{L}`$ — least snapshots a link stays off once stopped | +| $`\mathrm{u}^{f,0}`$ | `Link_status_initial` over $`\mathcal{L}`$ — one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{hold}^{f}`$ | `Link_must_stay_up` over $`\mathcal{T} \times \mathcal{L}`$ — true while the up time a link brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | +| $`\mathrm{rest}^{f}`$ | `Link_must_stay_down` over $`\mathcal{T} \times \mathcal{L}`$ — true while the down time a link brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | +| $`\mathrm{c}^{f,\mathrm{up}}`$ | `Link_start_up_cost` over $`\mathcal{L}`$ — cost of one start | +| $`\mathrm{c}^{f,\mathrm{dn}}`$ | `Link_shut_down_cost` over $`\mathcal{L}`$ — cost of one stop | +| $`\mathrm{c}^{f,\mathrm{on}}`$ | `Link_stand_by_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one snapshot spent on | +| $`\mathrm{f}^{\mathrm{mod}}`$ | `Link_p_nom_mod` over $`\mathcal{L}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{N}^{f,\mathrm{fix}}`$ | `Link_modules_installed` over $`\mathcal{L}`$ — how many whole modules a committable build has in place: `Link_p_nom / Link_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | +| $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\mathcal{L}`$ — a bound safely above any feasible flow — the build cap at full availability, data prep | +| $`\mathrm{nonneg}^{f}`$ | `Link_p_min_pu_nonneg` over $`\mathcal{L}`$ — true where none of the link's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | | $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\mathcal{J}`$ — nominal internal power | | $`\mathrm{ext}^{z}`$ | `Process_p_nom_extendable` over $`\mathcal{J}`$ — whether the nominal internal power is a decision | | $`\underline{\mathrm{z}}`$ | `Process_p_min_pu` over $`\mathcal{T} \times \mathcal{J}`$ — least internal power, per unit of nominal power — negative for a process that runs both ways | @@ -2269,6 +2412,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`u`$ | `Generator_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-status` — how much of a committable unit is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}`$ | `Generator_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | | $`\mathit{dn}`$ | `Generator_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | +| $`N^{f}`$ | `Link_n_mod` over $`\mathcal{L}`$ — `Link-n_mod` — how many modules of an extendable modular build | +| $`u^{f}`$ | `Link_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-status` — how much of a committable link is on: an integer the rows below cap at one, or at the module count where the build is modular | +| $`\mathit{up}^{f}`$ | `Link_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-start_up` — how much of a committable link turns on this snapshot, capped as the status is | +| $`\mathit{dn}^{f}`$ | `Link_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-shut_down` — how much of a committable link turns off this snapshot, capped as the status is | | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | | $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | @@ -2295,6 +2442,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\Delta^{+}`$ | `Generator_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | +| $`\overleftarrow{u}^{f}`$ | `Link_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the commitment state a link carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | +| $`\overleftarrow{f}`$ | `Link_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the flow a link carries into a snapshot — nothing at the start of the horizon, which is why a link that came in running carries no ramp row there | +| $`\Delta^{f,+}`$ | `Link_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may raise flow between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | +| $`\Delta^{f,-}`$ | `Link_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may lower flow between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | | $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | @@ -2378,12 +2529,12 @@ p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} Link_fix_p_lower: description: "`Link-fix-p-lower` — a fixed link carries at least its minimum, negative for the other way" dims: [scenario, snapshot, link] - where: not Link_p_nom_extendable AND Link_active + where: not Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-fix-p-upper` @@ -2394,12 +2545,12 @@ f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} Link_fix_p_upper: description: "`Link-fix-p-upper` — a fixed link carries at most its nominal power" dims: [scenario, snapshot, link] - where: not Link_p_nom_extendable AND Link_active + where: not Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Generator-ext-p-lower` @@ -2522,12 +2673,12 @@ Generator_e_sum_max: Link_ext_p_lower: description: "`Link-ext-p-lower` — an extendable link carries at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, link] - where: Link_p_nom_extendable AND Link_active + where: Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom_ext ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p-upper` @@ -2538,12 +2689,12 @@ f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \i Link_ext_p_upper: description: "`Link-ext-p-upper` — an extendable link carries at most the chosen build" dims: [scenario, snapshot, link] - where: Link_p_nom_extendable AND Link_active + where: Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom_ext ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p_nom-lower` @@ -3269,6 +3420,471 @@ Generator_shut_down_p_nom_variable_upper: \mathit{dn}_{\xi,t,g} \le N_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` +### `Link-com-p-lower` + +`Link_com_p_lower` + +```yaml +Link_com_p_lower: + description: "`Link-com-p-lower` — a committed link flows at least its minimum; off, at least nothing" + dims: [scenario, snapshot, link] + where: Link_committable AND not Link_p_nom_extendable AND Link_active + expression: Link_p >= Link_p_min_pu * Link_p_nom * Link_status +``` + +```math +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-p-upper` + +`Link_com_p_upper` + +```yaml +Link_com_p_upper: + description: "`Link-com-p-upper` — a committed link flows at most what is available; off, at most nothing" + dims: [scenario, snapshot, link] + where: Link_committable AND not Link_p_nom_extendable AND Link_active + expression: Link_p <= Link_p_max_pu * Link_p_nom * Link_status +``` + +```math +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-transition-start-up` + +`Link_com_transition_start_up` + +```yaml +Link_com_transition_start_up: + description: "`Link-com-transition-start-up` — turning on is a start, counted against the state the link carried into the snapshot" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_active + expression: Link_start_up >= Link_status - Link_previous_status +``` + +```math +\mathit{up}^{f}_{\xi,t,l} \ge u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-transition-shut-down` + +`Link_com_transition_shut_down` + +```yaml +Link_com_transition_shut_down: + description: "`Link-com-transition-shut-down` — turning off is a stop, counted against the state the link carried into the snapshot" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_active + expression: Link_shut_down >= Link_previous_status - Link_status +``` + +```math +\mathit{dn}^{f}_{\xi,t,l} \ge \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-up-time` + +`Link_com_up_time` + +```yaml +Link_com_up_time: + description: >- + `Link-com-up-time` — a link started within its own minimum up time + is still on. The first snapshot's share of the window is the brought-in + up time's, which the must-stay-up mask carries + dims: [scenario, snapshot, link] + where: Link_committable AND Link_min_up_time > 0 AND position(snapshot) > 0 AND Link_active + expression: sum_back(Link_start_up, along=snapshot, window=Link_min_up_time) <= Link_status +``` + +```math +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}^{f}} \mathit{up}^{f}_{\xi,t',l} \le u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{UT}^{f}_{l} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-down-time` + +`Link_com_down_time` + +```yaml +Link_com_down_time: + description: >- + `Link-com-down-time` — a link stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries + dims: [scenario, snapshot, link] + where: Link_committable AND Link_min_down_time > 0 AND position(snapshot) > 0 AND Link_active + expression: sum_back(Link_shut_down, along=snapshot, window=Link_min_down_time) <= 1 - Link_status +``` + +```math +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}^{f}} \mathit{dn}^{f}_{\xi,t',l} \le 1 - u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{DT}^{f}_{l} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-status-min_up_time_must_stay_up` + +`Link_com_status_must_stay_up` + +```yaml +Link_com_status_must_stay_up: + description: "`Link-com-status-min_up_time_must_stay_up` — a link still serving the up time it brought in stays on" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_must_stay_up AND Link_active + expression: Link_status == 1 +``` + +```math +u^{f}_{\xi,t,l} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{hold}^{f}_{t,l} \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-status-min_down_time_must_stay_up` + +`Link_com_status_must_stay_down` + +```yaml +Link_com_status_must_stay_down: + description: >- + `Link-com-status-min_down_time_must_stay_up` — a link still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [scenario, snapshot, link] + where: Link_committable AND Link_must_stay_down AND Link_active + expression: Link_status == 0 +``` + +```math +u^{f}_{\xi,t,l} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{rest}^{f}_{t,l} \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-p-ramp_limit_up-run-bigM` + +`Link_p_ramp_limit_up_run_big_m` + +```yaml +Link_p_ramp_limit_up_run_big_m: + description: >- + `Link-p-ramp_limit_up-run-bigM` — a committed extendable link + raises flow no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns on + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_p - Link_previous_p <= + Link_ramp_limit_up * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_previous_status +``` + +```math +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \mathrm{ru}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-p-ramp_limit_up-start-bigM` + +`Link_p_ramp_limit_up_start_big_m` + +```yaml +Link_p_ramp_limit_up_start_big_m: + description: >- + `Link-p-ramp_limit_up-start-bigM` — in the snapshot it turns on, a + committed extendable link ramps no further than its start-up ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_p - Link_previous_p <= + Link_ramp_limit_start_up * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_start_up +``` + +```math +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \mathrm{ru}^{f,\mathrm{up}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-p-ramp_limit_down-run-bigM` + +`Link_p_ramp_limit_down_run_big_m` + +```yaml +Link_p_ramp_limit_down_run_big_m: + description: >- + `Link-p-ramp_limit_down-run-bigM` — a committed extendable link + lowers flow no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns off + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_previous_p - Link_p <= + Link_ramp_limit_down * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_status +``` + +```math +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \mathrm{rd}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-p-ramp_limit_down-shut-bigM` + +`Link_p_ramp_limit_down_shut_big_m` + +```yaml +Link_p_ramp_limit_down_shut_big_m: + description: >- + `Link-p-ramp_limit_down-shut-bigM` — in the snapshot it turns off, + a committed extendable link ramps no further than its shut-down ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_previous_p - Link_p <= + Link_ramp_limit_shut_down * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_shut_down +``` + +```math +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \mathrm{rd}^{f,\mathrm{dn}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-p_nom_modularity` + +`Link_p_nom_modularity` + +```yaml +Link_p_nom_modularity: + description: "`Link-p_nom_modularity` — the chosen build is a whole number of modules" + dims: [link] + where: Link_p_nom_extendable AND Link_p_nom_mod > 0 + expression: Link_p_nom_ext == Link_p_nom_mod * Link_n_mod +``` + +```math +F_{l} = \mathrm{f}^{\mathrm{mod}}_{l} \cdot N^{f}_{l} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 +``` + +### `Link-com-ext-p-upper-cap` + +`Link_com_ext_p_upper_cap` + +```yaml +Link_com_ext_p_upper_cap: + description: >- + `Link-com-ext-p-upper-cap` — a committed extendable link flows + at most what is available of the chosen build, whatever its status + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: Link_p <= Link_p_max_pu * Link_p_nom_ext +``` + +```math +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-ext-p-upper-bigM` + +`Link_com_ext_p_upper_big_m` + +```yaml +Link_com_ext_p_upper_big_m: + description: "`Link-com-ext-p-upper-bigM` — off, a link flows nothing; on, the big M is no bound" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: Link_p <= Link_big_m * Link_status +``` + +```math +f_{\xi,t,l} \le \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-ext-p-lower` + +`Link_com_ext_p_lower` + +```yaml +Link_com_ext_p_lower: + description: >- + `Link-com-ext-p-lower` — a committed extendable link flows at + least its minimum of the chosen build; off, the big M releases the row + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: >- + Link_p >= + Link_p_min_pu * Link_p_nom_ext + + Link_big_m * Link_status - Link_big_m +``` + +```math +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} + \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} - \mathrm{M}^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-ext-p-lower-nonneg` + +`Link_com_ext_p_lower_nonneg` + +```yaml +Link_com_ext_p_lower_nonneg: + description: >- + `Link-com-ext-p-lower-nonneg` — where no minimum-per-unit is + negative, flow is also plainly non-negative, a row the big-M lower + cannot assert while the link is off + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable + AND Link_p_min_pu_nonneg AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: Link_p >= 0 +``` + +```math +f_{\xi,t,l} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{nonneg}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-mod-p-lower` + +`Link_com_mod_p_lower` + +```yaml +Link_com_mod_p_lower: + description: >- + `Link-com-mod-p-lower` — a committed modular link flows at least + its minimum of one module, whether the build is fixed or a decision + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_p >= Link_p_min_pu * Link_p_nom_mod * Link_status +``` + +```math +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-mod-p-upper` + +`Link_com_mod_p_upper` + +```yaml +Link_com_mod_p_upper: + description: >- + `Link-com-mod-p-upper` — a committed modular link flows at most + one module's share, whether the build is fixed or a decision + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_p <= Link_p_max_pu * Link_p_nom_mod * Link_status +``` + +```math +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-status-p-fixed-upper` + +`Link_status_p_fixed_upper` + +```yaml +Link_status_p_fixed_upper: + description: >- + `Link-status-p-fixed-upper` — a status is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_status <= Link_modules_installed +``` + +```math +u^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-start_up-p-fixed-upper` + +`Link_start_up_p_fixed_upper` + +```yaml +Link_start_up_p_fixed_upper: + description: >- + `Link-start_up-p-fixed-upper` — a start is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_start_up <= Link_modules_installed +``` + +```math +\mathit{up}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-shut_down-p-fixed-upper` + +`Link_shut_down_p_fixed_upper` + +```yaml +Link_shut_down_p_fixed_upper: + description: >- + `Link-shut_down-p-fixed-upper` — a stop is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_shut_down <= Link_modules_installed +``` + +```math +\mathit{dn}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-status-p_nom-variable-upper` + +`Link_status_p_nom_variable_upper` + +```yaml +Link_status_p_nom_variable_upper: + description: "`Link-status-p_nom-variable-upper` — a modular link is on only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_status <= Link_n_mod +``` + +```math +u^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-start_up-p_nom-variable-upper` + +`Link_start_up_p_nom_variable_upper` + +```yaml +Link_start_up_p_nom_variable_upper: + description: "`Link-start_up-p_nom-variable-upper` — a modular link starts only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_start_up <= Link_n_mod +``` + +```math +\mathit{up}^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-shut_down-p_nom-variable-upper` + +`Link_shut_down_p_nom_variable_upper` + +```yaml +Link_shut_down_p_nom_variable_upper: + description: "`Link-shut_down-p_nom-variable-upper` — a modular link stops only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_shut_down <= Link_n_mod +``` + +```math +\mathit{dn}^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + ### `Line-fix-s-lower` `Line_fix_s_lower` @@ -3712,16 +4328,21 @@ Generator_p_ramp_limit_down: ```yaml Link_p_ramp_limit_up: description: >- - `Link-p-ramp_limit_up` — a link raises flow no faster than its limit of - the build. The translated term vacates the first snapshot, where a plain - optimize builds no row either + `Link-p-ramp_limit_up` — a link raises flow no faster than + its ramp limit of the build, and a committed one no further than its + start-up ramp in the snapshot it turns on. A link that came into the + horizon running brought an unknown flow, so it carries no row at the + first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] - where: Link_ramp_limit_up AND Link_active - expression: Link_p - shift(Link_p, along=snapshot, offset=1) <= Link_ramp_limit_up * Link_p_nom_effective + where: >- + Link_ramp_limit_up + AND NOT (Link_committable AND Link_p_nom_extendable) + AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + expression: Link_p - Link_previous_p <= Link_ramp_up_allowance ``` ```math -f_{\xi,t,l} - f_{\xi,t - 1,l} \le \mathrm{ru}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down` @@ -3730,14 +4351,22 @@ f_{\xi,t,l} - f_{\xi,t - 1,l} \le \mathrm{ru}^{f}_{l} \cdot \widetilde{\mathrm{f ```yaml Link_p_ramp_limit_down: - description: "`Link-p-ramp_limit_down` — a link lowers flow no faster than its limit of the build" + description: >- + `Link-p-ramp_limit_down` — a link lowers flow no faster than + its ramp limit of the build, and a committed one no further than its + shut-down ramp in the snapshot it turns off. A link that came into the + horizon running brought an unknown flow, so it carries no row at the + first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] - where: Link_ramp_limit_down AND Link_active - expression: shift(Link_p, along=snapshot, offset=1) - Link_p <= Link_ramp_limit_down * Link_p_nom_effective + where: >- + Link_ramp_limit_down + AND NOT (Link_committable AND Link_p_nom_extendable) + AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + expression: Link_previous_p - Link_p <= Link_ramp_down_allowance ``` ```math -f_{\xi,t - 1,l} - f_{\xi,t,l} \le \mathrm{rd}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-p-ramp_limit_up` @@ -4615,6 +5244,87 @@ Link_p_nom_effective: \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} = \begin{cases} F_{l} & \text{if } \mathrm{ext}^{f}_{l} \\ \mathrm{f}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} ``` +### `Link_previous_status` + +```yaml +Link_previous_status: + description: >- + the commitment state a link carries into a snapshot — the state it + brought into the horizon at the first, the previous snapshot's after that + dims: [scenario, snapshot, link] + cases: + opening: { when: "position(snapshot) == 0", expression: Link_status_initial } + otherwise: shift(Link_status, along=snapshot, offset=1) +``` + +```math +\overleftarrow{u}^{f}_{\xi,t,l} = \begin{cases} \mathrm{u}^{f,0}_{l} & \text{if } \mathrm{pos}(t) = 0 \\ u^{f}_{\xi,t - 1,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +``` + +### `Link_previous_p` + +```yaml +Link_previous_p: + description: >- + the flow a link carries into a snapshot — nothing at the start of + the horizon, which is why a link that came in running carries no ramp row + there + dims: [scenario, snapshot, link] + cases: + opening: { when: "position(snapshot) == 0", expression: 0 } + otherwise: shift(Link_p, along=snapshot, offset=1) +``` + +```math +\overleftarrow{f}_{\xi,t,l} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ f_{\xi,t - 1,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +``` + +### `Link_ramp_up_allowance` + +```yaml +Link_ramp_up_allowance: + description: >- + how far a link may raise flow between two snapshots — its ramp + limit of the build while it stays on, plus its start-up ramp in the + snapshot it turns on + dims: [scenario, snapshot, link] + cases: + committed: + when: Link_committable + expression: >- + Link_ramp_limit_up * Link_p_nom * Link_previous_status + + Link_ramp_limit_start_up * Link_p_nom + * (Link_status - Link_previous_status) + otherwise: Link_ramp_limit_up * Link_p_nom_effective +``` + +```math +\Delta^{f,+}_{\xi,t,l} = \begin{cases} \mathrm{ru}^{f}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} + \mathrm{ru}^{f,\mathrm{up}}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \mathrm{ru}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +``` + +### `Link_ramp_down_allowance` + +```yaml +Link_ramp_down_allowance: + description: >- + how far a link may lower flow between two snapshots — its ramp + limit of the build while it stays on, plus its shut-down ramp in the + snapshot it turns off + dims: [scenario, snapshot, link] + cases: + committed: + when: Link_committable + expression: >- + Link_ramp_limit_down * Link_p_nom * Link_status + + Link_ramp_limit_shut_down * Link_p_nom + * (Link_previous_status - Link_status) + otherwise: Link_ramp_limit_down * Link_p_nom_effective +``` + +```math +\Delta^{f,-}_{\xi,t,l} = \begin{cases} \mathrm{rd}^{f}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} + \mathrm{rd}^{f,\mathrm{dn}}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \mathrm{rd}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +``` + ### `Process_p_nom_effective` ```yaml @@ -4824,10 +5534,13 @@ scenario_opex: + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_shut_down * Generator_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Link_status * Link_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_start_up * Link_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_shut_down * Link_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) ``` ```math -\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Carrier_additions` @@ -4933,6 +5646,30 @@ u_{\xi,t,g} \ge 0, u_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \ \mathit{dn}_{\xi,t,g} \ge 0, \mathit{dn}_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` +**`Link_n_mod`** + +```math +N^{f}_{l} \ge 0, N^{f}_{l} \in \mathbb{Z} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 +``` + +**`Link_status`** + +```math +u^{f}_{\xi,t,l} \ge 0, u^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +**`Link_start_up`** + +```math +\mathit{up}^{f}_{\xi,t,l} \ge 0, \mathit{up}^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +**`Link_shut_down`** + +```math +\mathit{dn}^{f}_{\xi,t,l} \ge 0, \mathit{dn}^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + **`Line_s`** ```math diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index bd1fe383..3e9846a4 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -201,6 +201,70 @@ parameters: Link_marginal_cost_quadratic: description: cost of the square of one unit of flow dims: [snapshot, link] + Link_committable: + description: whether flow is gated by an on/off status decision + dims: [link] + dtype: bool + Link_ramp_limit_start_up: + description: most flow in the snapshot a link starts, per unit of nominal power + dims: [link] + Link_ramp_limit_shut_down: + description: most flow in the snapshot before a link stops, per unit of nominal power + dims: [link] + Link_min_up_time: + description: least snapshots a link stays on once started + dims: [link] + dtype: int + Link_min_down_time: + description: least snapshots a link stays off once stopped + dims: [link] + dtype: int + Link_status_initial: + description: one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep + dims: [link] + dtype: int + Link_must_stay_up: + description: >- + true while the up time a link brought into the horizon still binds — + data prep, since `position()` compares against a literal rather than a + parameter + dims: [snapshot, link] + dtype: bool + Link_must_stay_down: + description: >- + true while the down time a link brought into the horizon still binds — + PyPSA's `min_down_time - down_time_before` snapshots, where + `down_time_before > 0`, data prep for the same reason + dims: [snapshot, link] + dtype: bool + Link_start_up_cost: + description: cost of one start + dims: [link] + Link_shut_down_cost: + description: cost of one stop + dims: [link] + Link_stand_by_cost: + description: cost of one snapshot spent on + dims: [snapshot, link] + Link_p_nom_mod: + description: the module size a build comes in whole numbers of; no value means the build is continuous + dims: [link] + Link_modules_installed: + description: >- + how many whole modules a committable build has in place: `Link_p_nom + / Link_p_nom_mod` where a fixed build is modular, one where it is + not, data prep. PyPSA refuses a fixed modular build whose nominal power + is not a whole number of modules + dims: [link] + Link_big_m: + description: a bound safely above any feasible flow — the build cap at full availability, data prep + dims: [link] + Link_p_min_pu_nonneg: + description: >- + true where none of the link's own minimums-per-unit is negative — + PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep + dims: [link] + dtype: bool Process_p_nom: description: nominal internal power dims: [process] @@ -880,6 +944,37 @@ variables: domain: integer bounds: lower: 0 + Link_n_mod: + description: "`Link-n_mod` — how many modules of an extendable modular build" + dims: [link] + where: Link_p_nom_extendable AND Link_p_nom_mod > 0 + domain: integer + bounds: + lower: 0 + Link_status: + description: >- + `Link-status` — how much of a committable link is on: an integer + the rows below cap at one, or at the module count where the build is + modular + dims: [scenario, snapshot, link] + where: Link_committable AND Link_active + domain: integer + bounds: + lower: 0 + Link_start_up: + description: "`Link-start_up` — how much of a committable link turns on this snapshot, capped as the status is" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_active + domain: integer + bounds: + lower: 0 + Link_shut_down: + description: "`Link-shut_down` — how much of a committable link turns off this snapshot, capped as the status is" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_active + domain: integer + bounds: + lower: 0 Line_s: description: >- `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a @@ -1037,6 +1132,51 @@ expressions: cases: extendable: { when: Link_p_nom_extendable, expression: Link_p_nom_ext } otherwise: Link_p_nom + Link_previous_status: + description: >- + the commitment state a link carries into a snapshot — the state it + brought into the horizon at the first, the previous snapshot's after that + dims: [scenario, snapshot, link] + cases: + opening: { when: "position(snapshot) == 0", expression: Link_status_initial } + otherwise: shift(Link_status, along=snapshot, offset=1) + Link_previous_p: + description: >- + the flow a link carries into a snapshot — nothing at the start of + the horizon, which is why a link that came in running carries no ramp row + there + dims: [scenario, snapshot, link] + cases: + opening: { when: "position(snapshot) == 0", expression: 0 } + otherwise: shift(Link_p, along=snapshot, offset=1) + Link_ramp_up_allowance: + description: >- + how far a link may raise flow between two snapshots — its ramp + limit of the build while it stays on, plus its start-up ramp in the + snapshot it turns on + dims: [scenario, snapshot, link] + cases: + committed: + when: Link_committable + expression: >- + Link_ramp_limit_up * Link_p_nom * Link_previous_status + + Link_ramp_limit_start_up * Link_p_nom + * (Link_status - Link_previous_status) + otherwise: Link_ramp_limit_up * Link_p_nom_effective + Link_ramp_down_allowance: + description: >- + how far a link may lower flow between two snapshots — its ramp + limit of the build while it stays on, plus its shut-down ramp in the + snapshot it turns off + dims: [scenario, snapshot, link] + cases: + committed: + when: Link_committable + expression: >- + Link_ramp_limit_down * Link_p_nom * Link_status + + Link_ramp_limit_shut_down * Link_p_nom + * (Link_previous_status - Link_status) + otherwise: Link_ramp_limit_down * Link_p_nom_effective Process_p_nom_effective: description: the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise dims: [process] @@ -1153,6 +1293,9 @@ expressions: + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_shut_down * Generator_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Link_status * Link_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_start_up * Link_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_shut_down * Link_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) Carrier_additions: description: >- what a carrier adds in a period — every extendable component of that @@ -1182,12 +1325,12 @@ constraints: Link_fix_p_lower: description: "`Link-fix-p-lower` — a fixed link carries at least its minimum, negative for the other way" dims: [scenario, snapshot, link] - where: not Link_p_nom_extendable AND Link_active + where: not Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom Link_fix_p_upper: description: "`Link-fix-p-upper` — a fixed link carries at most its nominal power" dims: [scenario, snapshot, link] - where: not Link_p_nom_extendable AND Link_active + where: not Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom Generator_ext_p_lower: description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" @@ -1227,12 +1370,12 @@ constraints: Link_ext_p_lower: description: "`Link-ext-p-lower` — an extendable link carries at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, link] - where: Link_p_nom_extendable AND Link_active + where: Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p >= Link_p_min_pu * Link_p_nom_ext Link_ext_p_upper: description: "`Link-ext-p-upper` — an extendable link carries at most the chosen build" dims: [scenario, snapshot, link] - where: Link_p_nom_extendable AND Link_active + where: Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p <= Link_p_max_pu * Link_p_nom_ext Link_ext_p_nom_lower: description: "`Link-ext-p_nom-lower` — the chosen build is at least its floor" @@ -1506,6 +1649,196 @@ constraints: dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_shut_down <= Generator_n_mod + Link_com_p_lower: + description: "`Link-com-p-lower` — a committed link flows at least its minimum; off, at least nothing" + dims: [scenario, snapshot, link] + where: Link_committable AND not Link_p_nom_extendable AND Link_active + expression: Link_p >= Link_p_min_pu * Link_p_nom * Link_status + Link_com_p_upper: + description: "`Link-com-p-upper` — a committed link flows at most what is available; off, at most nothing" + dims: [scenario, snapshot, link] + where: Link_committable AND not Link_p_nom_extendable AND Link_active + expression: Link_p <= Link_p_max_pu * Link_p_nom * Link_status + Link_com_transition_start_up: + description: "`Link-com-transition-start-up` — turning on is a start, counted against the state the link carried into the snapshot" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_active + expression: Link_start_up >= Link_status - Link_previous_status + Link_com_transition_shut_down: + description: "`Link-com-transition-shut-down` — turning off is a stop, counted against the state the link carried into the snapshot" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_active + expression: Link_shut_down >= Link_previous_status - Link_status + Link_com_up_time: + description: >- + `Link-com-up-time` — a link started within its own minimum up time + is still on. The first snapshot's share of the window is the brought-in + up time's, which the must-stay-up mask carries + dims: [scenario, snapshot, link] + where: Link_committable AND Link_min_up_time > 0 AND position(snapshot) > 0 AND Link_active + expression: sum_back(Link_start_up, along=snapshot, window=Link_min_up_time) <= Link_status + Link_com_down_time: + description: >- + `Link-com-down-time` — a link stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries + dims: [scenario, snapshot, link] + where: Link_committable AND Link_min_down_time > 0 AND position(snapshot) > 0 AND Link_active + expression: sum_back(Link_shut_down, along=snapshot, window=Link_min_down_time) <= 1 - Link_status + Link_com_status_must_stay_up: + description: "`Link-com-status-min_up_time_must_stay_up` — a link still serving the up time it brought in stays on" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_must_stay_up AND Link_active + expression: Link_status == 1 + Link_com_status_must_stay_down: + description: >- + `Link-com-status-min_down_time_must_stay_up` — a link still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [scenario, snapshot, link] + where: Link_committable AND Link_must_stay_down AND Link_active + expression: Link_status == 0 + Link_p_ramp_limit_up_run_big_m: + description: >- + `Link-p-ramp_limit_up-run-bigM` — a committed extendable link + raises flow no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns on + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_p - Link_previous_p <= + Link_ramp_limit_up * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_previous_status + Link_p_ramp_limit_up_start_big_m: + description: >- + `Link-p-ramp_limit_up-start-bigM` — in the snapshot it turns on, a + committed extendable link ramps no further than its start-up ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_p - Link_previous_p <= + Link_ramp_limit_start_up * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_start_up + Link_p_ramp_limit_down_run_big_m: + description: >- + `Link-p-ramp_limit_down-run-bigM` — a committed extendable link + lowers flow no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns off + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_previous_p - Link_p <= + Link_ramp_limit_down * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_status + Link_p_ramp_limit_down_shut_big_m: + description: >- + `Link-p-ramp_limit_down-shut-bigM` — in the snapshot it turns off, + a committed extendable link ramps no further than its shut-down ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + expression: >- + Link_previous_p - Link_p <= + Link_ramp_limit_shut_down * Link_p_nom_ext + + Link_big_m - Link_big_m * Link_shut_down + Link_p_nom_modularity: + description: "`Link-p_nom_modularity` — the chosen build is a whole number of modules" + dims: [link] + where: Link_p_nom_extendable AND Link_p_nom_mod > 0 + expression: Link_p_nom_ext == Link_p_nom_mod * Link_n_mod + Link_com_ext_p_upper_cap: + description: >- + `Link-com-ext-p-upper-cap` — a committed extendable link flows + at most what is available of the chosen build, whatever its status + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: Link_p <= Link_p_max_pu * Link_p_nom_ext + Link_com_ext_p_upper_big_m: + description: "`Link-com-ext-p-upper-bigM` — off, a link flows nothing; on, the big M is no bound" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: Link_p <= Link_big_m * Link_status + Link_com_ext_p_lower: + description: >- + `Link-com-ext-p-lower` — a committed extendable link flows at + least its minimum of the chosen build; off, the big M releases the row + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: >- + Link_p >= + Link_p_min_pu * Link_p_nom_ext + + Link_big_m * Link_status - Link_big_m + Link_com_ext_p_lower_nonneg: + description: >- + `Link-com-ext-p-lower-nonneg` — where no minimum-per-unit is + negative, flow is also plainly non-negative, a row the big-M lower + cannot assert while the link is off + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable + AND Link_p_min_pu_nonneg AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: Link_p >= 0 + Link_com_mod_p_lower: + description: >- + `Link-com-mod-p-lower` — a committed modular link flows at least + its minimum of one module, whether the build is fixed or a decision + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_p >= Link_p_min_pu * Link_p_nom_mod * Link_status + Link_com_mod_p_upper: + description: >- + `Link-com-mod-p-upper` — a committed modular link flows at most + one module's share, whether the build is fixed or a decision + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_p <= Link_p_max_pu * Link_p_nom_mod * Link_status + Link_status_p_fixed_upper: + description: >- + `Link-status-p-fixed-upper` — a status is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_status <= Link_modules_installed + Link_start_up_p_fixed_upper: + description: >- + `Link-start_up-p-fixed-upper` — a start is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_start_up <= Link_modules_installed + Link_shut_down_p_fixed_upper: + description: >- + `Link-shut_down-p-fixed-upper` — a stop is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_shut_down <= Link_modules_installed + Link_status_p_nom_variable_upper: + description: "`Link-status-p_nom-variable-upper` — a modular link is on only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_status <= Link_n_mod + Link_start_up_p_nom_variable_upper: + description: "`Link-start_up-p_nom-variable-upper` — a modular link starts only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_start_up <= Link_n_mod + Link_shut_down_p_nom_variable_upper: + description: "`Link-shut_down-p_nom-variable-upper` — a modular link stops only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_shut_down <= Link_n_mod Line_fix_s_lower: description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating, the loss counted against it" dims: [scenario, snapshot, line] @@ -1669,17 +2002,30 @@ constraints: expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance Link_p_ramp_limit_up: description: >- - `Link-p-ramp_limit_up` — a link raises flow no faster than its limit of - the build. The translated term vacates the first snapshot, where a plain - optimize builds no row either + `Link-p-ramp_limit_up` — a link raises flow no faster than + its ramp limit of the build, and a committed one no further than its + start-up ramp in the snapshot it turns on. A link that came into the + horizon running brought an unknown flow, so it carries no row at the + first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] - where: Link_ramp_limit_up AND Link_active - expression: Link_p - shift(Link_p, along=snapshot, offset=1) <= Link_ramp_limit_up * Link_p_nom_effective + where: >- + Link_ramp_limit_up + AND NOT (Link_committable AND Link_p_nom_extendable) + AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + expression: Link_p - Link_previous_p <= Link_ramp_up_allowance Link_p_ramp_limit_down: - description: "`Link-p-ramp_limit_down` — a link lowers flow no faster than its limit of the build" + description: >- + `Link-p-ramp_limit_down` — a link lowers flow no faster than + its ramp limit of the build, and a committed one no further than its + shut-down ramp in the snapshot it turns off. A link that came into the + horizon running brought an unknown flow, so it carries no row at the + first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] - where: Link_ramp_limit_down AND Link_active - expression: shift(Link_p, along=snapshot, offset=1) - Link_p <= Link_ramp_limit_down * Link_p_nom_effective + where: >- + Link_ramp_limit_down + AND NOT (Link_committable AND Link_p_nom_extendable) + AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + expression: Link_previous_p - Link_p <= Link_ramp_down_allowance Process_p_ramp_limit_up: description: >- `Process-p-ramp_limit_up` — a process raises internal power no faster than diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index c44f97ff..84716cc5 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -848,5 +848,56 @@ "Link-fix-p-lower": 4, "Link-fix-p-upper": 4 } + }, + "rung_25_committable_link": { + "columns": { + "Generator-p": 8, + "Link-n_mod": 1, + "Link-p": 24, + "Link-p_nom": 2, + "Link-shut_down": 20, + "Link-start_up": 20, + "Link-status": 20 + }, + "global_constraints": {}, + "objective": 14013.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Link-com-down-time": 6, + "Link-com-ext-p-lower": 4, + "Link-com-ext-p-lower-nonneg": 4, + "Link-com-ext-p-upper-bigM": 4, + "Link-com-ext-p-upper-cap": 4, + "Link-com-mod-p-lower": 8, + "Link-com-mod-p-upper": 8, + "Link-com-p-lower": 12, + "Link-com-p-upper": 12, + "Link-com-status-min_down_time_must_stay_up": 2, + "Link-com-status-min_up_time_must_stay_up": 2, + "Link-com-transition-shut-down": 20, + "Link-com-transition-start-up": 20, + "Link-com-up-time": 6, + "Link-ext-p_nom-lower": 2, + "Link-ext-p_nom-upper": 2, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Link-p-ramp_limit_down": 3, + "Link-p-ramp_limit_down-run-bigM": 4, + "Link-p-ramp_limit_down-shut-bigM": 4, + "Link-p-ramp_limit_up": 3, + "Link-p-ramp_limit_up-run-bigM": 4, + "Link-p-ramp_limit_up-start-bigM": 4, + "Link-p_nom_modularity": 1, + "Link-shut_down-p-fixed-upper": 16, + "Link-shut_down-p_nom-variable-upper": 4, + "Link-start_up-p-fixed-upper": 16, + "Link-start_up-p_nom-variable-upper": 4, + "Link-status-p-fixed-upper": 16, + "Link-status-p_nom-variable-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_25_committable_link.py b/examples/references/pypsa/rung_25_committable_link.py new file mode 100644 index 00000000..1004a07c --- /dev/null +++ b/examples/references/pypsa/rung_25_committable_link.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 25: committable links — a link held on by the up time it brought in, one held off by its down time, one kept on by its own up time, and committable builds that are extendable, modular or both.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus that only committable links serve.""" + n = spine.build() + n.add('Bus', 'east') + n.add( + 'Link', + 'hvdc', + bus0='north', + bus1='east', + committable=True, + p_nom=60, + p_min_pu=0.3, + marginal_cost=8, + min_up_time=3, + min_down_time=2, + up_time_before=1, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ramp_limit_start_up=0.6, + ramp_limit_shut_down=0.6, + start_up_cost=100, + shut_down_cost=50, + stand_by_cost=5, + ) + n.add( + 'Link', + 'cold_tie', + bus0='north', + bus1='east', + committable=True, + p_nom=40, + p_min_pu=0.2, + min_up_time=2, + min_down_time=3, + up_time_before=0, + down_time_before=1, + start_up_cost=20, + ) + n.add( + 'Link', + 'ext_tie', + bus0='north', + bus1='east', + committable=True, + p_nom_extendable=True, + p_nom_max=30, + capital_cost=5, + p_min_pu=0.2, + marginal_cost=2, + up_time_before=0, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ) + n.add( + 'Link', + 'mod_tie', + bus0='south', + bus1='east', + committable=True, + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=40, + capital_cost=3, + p_min_pu=0.5, + ) + n.add( + 'Link', + 'mod_fix', + bus0='north', + bus1='east', + committable=True, + p_nom=20, + p_nom_mod=10, + p_min_pu=0.5, + marginal_cost=1, + ) + n.add('Load', 'east_load', bus='east', p_set=[20, 70, 60, 5]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index e46daf0d..7afb5e96 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -68,6 +68,29 @@ names: Link_output_cyclic_delay: '\mathrm{cyc}^{f}' Link_marginal_cost: '\mathrm{c}^{f}' Link_marginal_cost_quadratic: '\mathrm{c}^{f,(2)}' + Link_committable: '\mathrm{com}^{f}' + Link_ramp_limit_start_up: '\mathrm{ru}^{f,\mathrm{up}}' + Link_ramp_limit_shut_down: '\mathrm{rd}^{f,\mathrm{dn}}' + Link_status: "u^{f}" + Link_start_up: '\mathit{up}^{f}' + Link_shut_down: '\mathit{dn}^{f}' + Link_min_up_time: '\mathrm{UT}^{f}' + Link_min_down_time: '\mathrm{DT}^{f}' + Link_status_initial: '\mathrm{u}^{f,0}' + Link_previous_status: '\overleftarrow{u}^{f}' + Link_previous_p: '\overleftarrow{f}' + Link_ramp_up_allowance: '\Delta^{f,+}' + Link_ramp_down_allowance: '\Delta^{f,-}' + Link_must_stay_up: '\mathrm{hold}^{f}' + Link_must_stay_down: '\mathrm{rest}^{f}' + Link_start_up_cost: '\mathrm{c}^{f,\mathrm{up}}' + Link_shut_down_cost: '\mathrm{c}^{f,\mathrm{dn}}' + Link_stand_by_cost: '\mathrm{c}^{f,\mathrm{on}}' + Link_p_nom_mod: '\mathrm{f}^{\mathrm{mod}}' + Link_n_mod: "N^{f}" + Link_modules_installed: '\mathrm{N}^{f,\mathrm{fix}}' + Link_big_m: '\mathrm{M}^{f}' + Link_p_min_pu_nonneg: '\mathrm{nonneg}^{f}' Load_p_set: '\mathrm{load}' Generator_p_set: '\mathrm{p}^{\mathrm{set}}' Link_p_set: '\mathrm{f}^{\mathrm{set}}' diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index c5acc4ce..88e01053 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -169,6 +169,48 @@ constraints: Line_s_set: - snapshot - line + Link_com_down_time: + - snapshot + - link + Link_com_ext_p_lower: + - snapshot + - link + Link_com_ext_p_lower_nonneg: + - snapshot + - link + Link_com_ext_p_upper_big_m: + - snapshot + - link + Link_com_ext_p_upper_cap: + - snapshot + - link + Link_com_mod_p_lower: + - snapshot + - link + Link_com_mod_p_upper: + - snapshot + - link + Link_com_p_lower: + - snapshot + - link + Link_com_p_upper: + - snapshot + - link + Link_com_status_must_stay_down: + - snapshot + - link + Link_com_status_must_stay_up: + - snapshot + - link + Link_com_transition_shut_down: + - snapshot + - link + Link_com_transition_start_up: + - snapshot + - link + Link_com_up_time: + - snapshot + - link Link_ext_p_lower: - snapshot - link @@ -185,17 +227,49 @@ constraints: Link_fix_p_upper: - snapshot - link + Link_p_nom_modularity: + - link Link_p_nom_set: - link Link_p_ramp_limit_down: - snapshot - link + Link_p_ramp_limit_down_run_big_m: + - snapshot + - link + Link_p_ramp_limit_down_shut_big_m: + - snapshot + - link Link_p_ramp_limit_up: - snapshot - link + Link_p_ramp_limit_up_run_big_m: + - snapshot + - link + Link_p_ramp_limit_up_start_big_m: + - snapshot + - link Link_p_set: - snapshot - link + Link_shut_down_p_fixed_upper: + - snapshot + - link + Link_shut_down_p_nom_variable_upper: + - snapshot + - link + Link_start_up_p_fixed_upper: + - snapshot + - link + Link_start_up_p_nom_variable_upper: + - snapshot + - link + Link_status_p_fixed_upper: + - snapshot + - link + Link_status_p_nom_variable_upper: + - snapshot + - link Process_ext_p_lower: - snapshot - process @@ -327,6 +401,10 @@ expressions: - Generator_ramp_up_allowance - Link_output_arrival - Link_p_nom_effective + - Link_previous_p + - Link_previous_status + - Link_ramp_down_allowance + - Link_ramp_up_allowance - Process_output_arrival - Process_p_nom_effective - StorageUnit_charge_carried_in @@ -461,8 +539,12 @@ parameters: Line_volume_weight: - global_constraint - line + Link_big_m: + - link Link_capital_cost: - link + Link_committable: + - link Link_efficiency: - link_output Link_expansion_cost_weight: @@ -474,6 +556,18 @@ parameters: Link_marginal_cost_quadratic: - snapshot - link + Link_min_down_time: + - link + Link_min_up_time: + - link + Link_modules_installed: + - link + Link_must_stay_down: + - snapshot + - link + Link_must_stay_up: + - snapshot + - link Link_output_cyclic_delay: - link_output Link_output_delay: @@ -484,6 +578,8 @@ parameters: Link_p_min_pu: - snapshot - link + Link_p_min_pu_nonneg: + - link Link_p_nom: - link Link_p_nom_extendable: @@ -492,6 +588,8 @@ parameters: - link Link_p_nom_min: - link + Link_p_nom_mod: + - link Link_p_nom_set: - link Link_p_set: @@ -499,8 +597,21 @@ parameters: - link Link_ramp_limit_down: - link + Link_ramp_limit_shut_down: + - link + Link_ramp_limit_start_up: + - link Link_ramp_limit_up: - link + Link_shut_down_cost: + - link + Link_stand_by_cost: + - snapshot + - link + Link_start_up_cost: + - link + Link_status_initial: + - link Link_tech_capacity_weight: - global_constraint - link @@ -747,11 +858,22 @@ variables: - line Line_s_nom_ext: - line + Link_n_mod: + - link Link_p: - snapshot - link Link_p_nom_ext: - link + Link_shut_down: + - snapshot + - link + Link_start_up: + - snapshot + - link + Link_status: + - snapshot + - link Process_p: - snapshot - process diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index 17af5401..452e9687 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -27,6 +27,7 @@ #: capacity is chosen once, before the future is known, so it spans no scenario SCENARIO_FREE_VARIABLES = { 'Generator_n_mod', + 'Link_n_mod', 'Generator_p_nom_ext', 'Link_p_nom_ext', 'StorageUnit_p_nom_ext', From 6da41d904eac3e4c6c62057907bc5d8629de8734 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 10:22:18 +0200 Subject: [PATCH 12/46] 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/examples/pypsa.md | 776 +++++++++++++++++- examples/pypsa.yaml | 368 ++++++++- examples/references/pypsa/references.json | 52 ++ .../pypsa/rung_26_committable_process.py | 94 +++ examples/symbols/pypsa.yaml | 23 + tests/fixtures/pypsa_standard_shape.yaml | 122 +++ tests/test_pypsa_collapses.py | 1 + 7 files changed, 1405 insertions(+), 31 deletions(-) create mode 100644 examples/references/pypsa/rung_26_committable_process.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 5e212b5d..10c50503 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -596,7 +596,7 @@ def build(): | PyPSA | status | note | | -------------------------------------------- | ------ | ------------------------------------------------------------- | -| [`{c}-status`, `-start_up`, `-shut_down`](#variable-domains) | done | Generator; Link in rung 25 | +| [`{c}-status`, `-start_up`, `-shut_down`](#variable-domains) | done | Generator; Link in rung 25, Process in rung 26 | | [`{c}-com-p-lower/upper`](#generator-com-p-lower) | done | | | [`{c}-*-p-fixed-upper`](#generator-status-p-fixed-upper) | done | status, start and stop each at most one, as explicit rows | | [`{c}-com-transition-start-up/shut-down`](#generator-com-transition-start-up) | done | the state carried into a snapshot is a cased quantity, so the first snapshot needs no block of its own | @@ -1543,7 +1543,7 @@ generator's and the link's, read over a converter. | --- | --- | --- | | [`Process-p`, `Process-p_nom`](#variable-domains) | done | internal power and capacity, as a link | | [`Process-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#process-fix-p-lower) | done | rungs 1 and 3, over a converter | -| [`Process-p-ramp_limit_*`](#process-p-ramp_limit_up) | done | rung 4, on a non-committable converter | +| [`Process-p-ramp_limit_*`](#process-p-ramp_limit_up) | done | rung 4, on a non-committable converter; committed in rung 26 | | [`Process-p_set`](#process-p_set) | done | a fixed internal power schedule | | [`Process-p_nom_set`](#process-p_nom_set) | done | a fixed built capacity | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | each port enters at its `rate` | @@ -2151,6 +2151,133 @@ def build(): +### Rung 26 — committable processes + +A committable process carries the same unit commitment over its internal power +`p`. The status gates `p`, not a port, so every port follows the status at its +own `rate`. This rung restates rung 25's links as processes that draw a quarter +more from the north than they deliver to the east. The same must-stay and up +time rules bind. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Process-status`, `-start_up`, `-shut_down`, `-n_mod`](#variable-domains) | done | as the link's, rung 25 | +| [`Process-com-p-*`, `-com-mod-p-*`, `-com-ext-p-*`](#process-com-p-lower) | done | a committable process leaves the `Process-fix-p-*` and `Process-ext-p-*` rows | +| [`Process-*-p-fixed-upper`, `-*-p_nom-variable-upper`](#process-status-p-fixed-upper) | done | | +| [`Process-com-transition-*`, `-com-up-time`, `-com-down-time`](#process-com-transition-start-up) | done | | +| [`Process-com-status-min_up_time_must_stay_up`, `-min_down_time_must_stay_up`](#process-com-status-min_up_time_must_stay_up) | done | prep masks, as the generator's | +| [`Process-p-ramp_limit_*`, `-*-bigM`](#process-p-ramp_limit_up) | done | the generator's cased allowance and big-M rows over internal power | +| [`Process-p_nom_modularity`](#process-p_nom_modularity) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `15956.125`, 235 rows. + +
+The network, as PyPSA code + +`rung_26_committable_process.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 26: committable processes — rung 25's committable links restated as processes that draw a quarter more than they deliver.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus that only committable processes serve.""" + n = spine.build() + n.add('Bus', 'east') + n.add( + 'Process', + 'warm_conv', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom=60, + p_min_pu=0.3, + marginal_cost=8, + min_up_time=3, + min_down_time=2, + up_time_before=1, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ramp_limit_start_up=0.6, + ramp_limit_shut_down=0.6, + start_up_cost=100, + shut_down_cost=50, + stand_by_cost=5, + ) + n.add( + 'Process', + 'cold_conv', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom=40, + p_min_pu=0.2, + min_up_time=2, + min_down_time=3, + up_time_before=0, + down_time_before=1, + start_up_cost=20, + ) + n.add( + 'Process', + 'ext_conv', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom_extendable=True, + p_nom_max=30, + capital_cost=5, + p_min_pu=0.2, + marginal_cost=2, + up_time_before=0, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ) + n.add( + 'Process', + 'mod_conv', + bus0='south', + bus1='east', + rate0=-1.25, + committable=True, + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=40, + capital_cost=3, + p_min_pu=0.5, + ) + n.add( + 'Process', + 'mod_fix', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom=20, + p_nom_mod=10, + p_min_pu=0.5, + marginal_cost=1, + ) + n.add('Load', 'east_load', bus='east', p_set=[20, 70, 60, 5]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2268,6 +2395,21 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_max` over $`\mathcal{J}`$ — most nominal power an extendable process may be built at | | $`\mathrm{c}^{\mathrm{cap},z}`$ | `Process_capital_cost` over $`\mathcal{J}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | | $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\mathcal{J}`$ — a given nominal power for an extendable process; one without a value has no row here | +| $`\mathrm{com}^{z}`$ | `Process_committable` over $`\mathcal{J}`$ — whether internal power is gated by an on/off status decision | +| $`\mathrm{ru}^{z,\mathrm{up}}`$ | `Process_ramp_limit_start_up` over $`\mathcal{J}`$ — most internal power in the snapshot a process starts, per unit of nominal power | +| $`\mathrm{rd}^{z,\mathrm{dn}}`$ | `Process_ramp_limit_shut_down` over $`\mathcal{J}`$ — most internal power in the snapshot before a process stops, per unit of nominal power | +| $`\mathrm{UT}^{z}`$ | `Process_min_up_time` over $`\mathcal{J}`$ — least snapshots a process stays on once started | +| $`\mathrm{DT}^{z}`$ | `Process_min_down_time` over $`\mathcal{J}`$ — least snapshots a process stays off once stopped | +| $`\mathrm{u}^{z,0}`$ | `Process_status_initial` over $`\mathcal{J}`$ — one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{hold}^{z}`$ | `Process_must_stay_up` over $`\mathcal{T} \times \mathcal{J}`$ — true while the up time a process brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | +| $`\mathrm{rest}^{z}`$ | `Process_must_stay_down` over $`\mathcal{T} \times \mathcal{J}`$ — true while the down time a process brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | +| $`\mathrm{c}^{z,\mathrm{up}}`$ | `Process_start_up_cost` over $`\mathcal{J}`$ — cost of one start | +| $`\mathrm{c}^{z,\mathrm{dn}}`$ | `Process_shut_down_cost` over $`\mathcal{J}`$ — cost of one stop | +| $`\mathrm{c}^{z,\mathrm{on}}`$ | `Process_stand_by_cost` over $`\mathcal{T} \times \mathcal{J}`$ — cost of one snapshot spent on | +| $`\mathrm{z}^{\mathrm{mod}}`$ | `Process_p_nom_mod` over $`\mathcal{J}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{N}^{z,\mathrm{fix}}`$ | `Process_modules_installed` over $`\mathcal{J}`$ — how many whole modules a committable build has in place: `Process_p_nom / Process_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | +| $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\mathcal{J}`$ — a bound safely above any feasible internal power — the build cap at full availability, data prep | +| $`\mathrm{nonneg}^{z}`$ | `Process_p_min_pu_nonneg` over $`\mathcal{J}`$ — true where none of the process's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | @@ -2416,6 +2558,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`u^{f}`$ | `Link_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-status` — how much of a committable link is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}^{f}`$ | `Link_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-start_up` — how much of a committable link turns on this snapshot, capped as the status is | | $`\mathit{dn}^{f}`$ | `Link_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-shut_down` — how much of a committable link turns off this snapshot, capped as the status is | +| $`N^{z}`$ | `Process_n_mod` over $`\mathcal{J}`$ — `Process-n_mod` — how many modules of an extendable modular build | +| $`u^{z}`$ | `Process_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-status` — how much of a committable process is on: an integer the rows below cap at one, or at the module count where the build is modular | +| $`\mathit{up}^{z}`$ | `Process_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-start_up` — how much of a committable process turns on this snapshot, capped as the status is | +| $`\mathit{dn}^{z}`$ | `Process_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-shut_down` — how much of a committable process turns off this snapshot, capped as the status is | | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | | $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | @@ -2447,6 +2593,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\Delta^{f,+}`$ | `Link_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may raise flow between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{f,-}`$ | `Link_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may lower flow between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | +| $`\overleftarrow{u}^{z}`$ | `Process_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the commitment state a process carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | +| $`\overleftarrow{z}`$ | `Process_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the internal power a process carries into a snapshot — nothing at the start of the horizon, which is why a process that came in running carries no ramp row there | +| $`\Delta^{z,+}`$ | `Process_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may raise internal power between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | +| $`\Delta^{z,-}`$ | `Process_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may lower internal power between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | | $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | | $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | @@ -2753,12 +2903,12 @@ F_{l} = \mathrm{f}^{\mathrm{nom,set}}_{l} \qquad \forall\, l \in \mathcal{L} \,: Process_fix_p_lower: description: "`Process-fix-p-lower` — a fixed process runs at least its minimum, negative for the other way" dims: [scenario, snapshot, process] - where: not Process_p_nom_extendable AND Process_active + where: not Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p >= Process_p_min_pu * Process_p_nom ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-fix-p-upper` @@ -2769,12 +2919,12 @@ z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} Process_fix_p_upper: description: "`Process-fix-p-upper` — a fixed process runs at most its nominal power" dims: [scenario, snapshot, process] - where: not Process_p_nom_extendable AND Process_active + where: not Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p <= Process_p_max_pu * Process_p_nom ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p-lower` @@ -2785,12 +2935,12 @@ z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} Process_ext_p_lower: description: "`Process-ext-p-lower` — an extendable process runs at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, process] - where: Process_p_nom_extendable AND Process_active + where: Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p >= Process_p_min_pu * Process_p_nom_ext ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p-upper` @@ -2801,12 +2951,12 @@ z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \i Process_ext_p_upper: description: "`Process-ext-p-upper` — an extendable process runs at most the chosen build" dims: [scenario, snapshot, process] - where: Process_p_nom_extendable AND Process_active + where: Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p <= Process_p_max_pu * Process_p_nom_ext ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p_nom-lower` @@ -3885,6 +4035,471 @@ Link_shut_down_p_nom_variable_upper: \mathit{dn}^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` +### `Process-com-p-lower` + +`Process_com_p_lower` + +```yaml +Process_com_p_lower: + description: "`Process-com-p-lower` — a committed process runs at least its minimum; off, at least nothing" + dims: [scenario, snapshot, process] + where: Process_committable AND not Process_p_nom_extendable AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom * Process_status +``` + +```math +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-p-upper` + +`Process_com_p_upper` + +```yaml +Process_com_p_upper: + description: "`Process-com-p-upper` — a committed process runs at most what is available; off, at most nothing" + dims: [scenario, snapshot, process] + where: Process_committable AND not Process_p_nom_extendable AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom * Process_status +``` + +```math +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-transition-start-up` + +`Process_com_transition_start_up` + +```yaml +Process_com_transition_start_up: + description: "`Process-com-transition-start-up` — turning on is a start, counted against the state the process carried into the snapshot" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_active + expression: Process_start_up >= Process_status - Process_previous_status +``` + +```math +\mathit{up}^{z}_{\xi,t,j} \ge u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-transition-shut-down` + +`Process_com_transition_shut_down` + +```yaml +Process_com_transition_shut_down: + description: "`Process-com-transition-shut-down` — turning off is a stop, counted against the state the process carried into the snapshot" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_active + expression: Process_shut_down >= Process_previous_status - Process_status +``` + +```math +\mathit{dn}^{z}_{\xi,t,j} \ge \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-up-time` + +`Process_com_up_time` + +```yaml +Process_com_up_time: + description: >- + `Process-com-up-time` — a process started within its own minimum up time + is still on. The first snapshot's share of the window is the brought-in + up time's, which the must-stay-up mask carries + dims: [scenario, snapshot, process] + where: Process_committable AND Process_min_up_time > 0 AND position(snapshot) > 0 AND Process_active + expression: sum_back(Process_start_up, along=snapshot, window=Process_min_up_time) <= Process_status +``` + +```math +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}^{z}} \mathit{up}^{z}_{\xi,t',j} \le u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{UT}^{z}_{j} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-down-time` + +`Process_com_down_time` + +```yaml +Process_com_down_time: + description: >- + `Process-com-down-time` — a process stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries + dims: [scenario, snapshot, process] + where: Process_committable AND Process_min_down_time > 0 AND position(snapshot) > 0 AND Process_active + expression: sum_back(Process_shut_down, along=snapshot, window=Process_min_down_time) <= 1 - Process_status +``` + +```math +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}^{z}} \mathit{dn}^{z}_{\xi,t',j} \le 1 - u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{DT}^{z}_{j} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-status-min_up_time_must_stay_up` + +`Process_com_status_must_stay_up` + +```yaml +Process_com_status_must_stay_up: + description: "`Process-com-status-min_up_time_must_stay_up` — a process still serving the up time it brought in stays on" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_must_stay_up AND Process_active + expression: Process_status == 1 +``` + +```math +u^{z}_{\xi,t,j} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{hold}^{z}_{t,j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-status-min_down_time_must_stay_up` + +`Process_com_status_must_stay_down` + +```yaml +Process_com_status_must_stay_down: + description: >- + `Process-com-status-min_down_time_must_stay_up` — a process still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [scenario, snapshot, process] + where: Process_committable AND Process_must_stay_down AND Process_active + expression: Process_status == 0 +``` + +```math +u^{z}_{\xi,t,j} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{rest}^{z}_{t,j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-p-ramp_limit_up-run-bigM` + +`Process_p_ramp_limit_up_run_big_m` + +```yaml +Process_p_ramp_limit_up_run_big_m: + description: >- + `Process-p-ramp_limit_up-run-bigM` — a committed extendable process + raises internal power no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns on + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_p - Process_previous_p <= + Process_ramp_limit_up * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_previous_status +``` + +```math +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \mathrm{ru}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-p-ramp_limit_up-start-bigM` + +`Process_p_ramp_limit_up_start_big_m` + +```yaml +Process_p_ramp_limit_up_start_big_m: + description: >- + `Process-p-ramp_limit_up-start-bigM` — in the snapshot it turns on, a + committed extendable process ramps no further than its start-up ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_p - Process_previous_p <= + Process_ramp_limit_start_up * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_start_up +``` + +```math +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \mathrm{ru}^{z,\mathrm{up}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-p-ramp_limit_down-run-bigM` + +`Process_p_ramp_limit_down_run_big_m` + +```yaml +Process_p_ramp_limit_down_run_big_m: + description: >- + `Process-p-ramp_limit_down-run-bigM` — a committed extendable process + lowers internal power no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns off + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_previous_p - Process_p <= + Process_ramp_limit_down * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_status +``` + +```math +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \mathrm{rd}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-p-ramp_limit_down-shut-bigM` + +`Process_p_ramp_limit_down_shut_big_m` + +```yaml +Process_p_ramp_limit_down_shut_big_m: + description: >- + `Process-p-ramp_limit_down-shut-bigM` — in the snapshot it turns off, + a committed extendable process ramps no further than its shut-down ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_previous_p - Process_p <= + Process_ramp_limit_shut_down * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_shut_down +``` + +```math +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \mathrm{rd}^{z,\mathrm{dn}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-p_nom_modularity` + +`Process_p_nom_modularity` + +```yaml +Process_p_nom_modularity: + description: "`Process-p_nom_modularity` — the chosen build is a whole number of modules" + dims: [process] + where: Process_p_nom_extendable AND Process_p_nom_mod > 0 + expression: Process_p_nom_ext == Process_p_nom_mod * Process_n_mod +``` + +```math +Z_{j} = \mathrm{z}^{\mathrm{mod}}_{j} \cdot N^{z}_{j} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 +``` + +### `Process-com-ext-p-upper-cap` + +`Process_com_ext_p_upper_cap` + +```yaml +Process_com_ext_p_upper_cap: + description: >- + `Process-com-ext-p-upper-cap` — a committed extendable process runs + at most what is available of the chosen build, whatever its status + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom_ext +``` + +```math +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-ext-p-upper-bigM` + +`Process_com_ext_p_upper_big_m` + +```yaml +Process_com_ext_p_upper_big_m: + description: "`Process-com-ext-p-upper-bigM` — off, a process does not run; on, the big M is no bound" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: Process_p <= Process_big_m * Process_status +``` + +```math +z_{\xi,t,j} \le \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-ext-p-lower` + +`Process_com_ext_p_lower` + +```yaml +Process_com_ext_p_lower: + description: >- + `Process-com-ext-p-lower` — a committed extendable process runs at + least its minimum of the chosen build; off, the big M releases the row + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: >- + Process_p >= + Process_p_min_pu * Process_p_nom_ext + + Process_big_m * Process_status - Process_big_m +``` + +```math +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot Z_{j} + \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} - \mathrm{M}^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-ext-p-lower-nonneg` + +`Process_com_ext_p_lower_nonneg` + +```yaml +Process_com_ext_p_lower_nonneg: + description: >- + `Process-com-ext-p-lower-nonneg` — where no minimum-per-unit is + negative, internal power is also plainly non-negative, a row the big-M lower + cannot assert while the process is off + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable + AND Process_p_min_pu_nonneg AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: Process_p >= 0 +``` + +```math +z_{\xi,t,j} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{nonneg}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-mod-p-lower` + +`Process_com_mod_p_lower` + +```yaml +Process_com_mod_p_lower: + description: >- + `Process-com-mod-p-lower` — a committed modular process runs at least + its minimum of one module, whether the build is fixed or a decision + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom_mod * Process_status +``` + +```math +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-com-mod-p-upper` + +`Process_com_mod_p_upper` + +```yaml +Process_com_mod_p_upper: + description: >- + `Process-com-mod-p-upper` — a committed modular process runs at most + one module's share, whether the build is fixed or a decision + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom_mod * Process_status +``` + +```math +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-status-p-fixed-upper` + +`Process_status_p_fixed_upper` + +```yaml +Process_status_p_fixed_upper: + description: >- + `Process-status-p-fixed-upper` — a status is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, process] + where: Process_committable AND NOT (Process_p_nom_extendable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_status <= Process_modules_installed +``` + +```math +u^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-start_up-p-fixed-upper` + +`Process_start_up_p_fixed_upper` + +```yaml +Process_start_up_p_fixed_upper: + description: >- + `Process-start_up-p-fixed-upper` — a start is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, process] + where: Process_committable AND NOT (Process_p_nom_extendable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_start_up <= Process_modules_installed +``` + +```math +\mathit{up}^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-shut_down-p-fixed-upper` + +`Process_shut_down_p_fixed_upper` + +```yaml +Process_shut_down_p_fixed_upper: + description: >- + `Process-shut_down-p-fixed-upper` — a stop is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, process] + where: Process_committable AND NOT (Process_p_nom_extendable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_shut_down <= Process_modules_installed +``` + +```math +\mathit{dn}^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-status-p_nom-variable-upper` + +`Process_status_p_nom_variable_upper` + +```yaml +Process_status_p_nom_variable_upper: + description: "`Process-status-p_nom-variable-upper` — a modular process is on only where a module is built" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_status <= Process_n_mod +``` + +```math +u^{z}_{\xi,t,j} \le N^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-start_up-p_nom-variable-upper` + +`Process_start_up_p_nom_variable_upper` + +```yaml +Process_start_up_p_nom_variable_upper: + description: "`Process-start_up-p_nom-variable-upper` — a modular process starts only where a module is built" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_start_up <= Process_n_mod +``` + +```math +\mathit{up}^{z}_{\xi,t,j} \le N^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-shut_down-p_nom-variable-upper` + +`Process_shut_down_p_nom_variable_upper` + +```yaml +Process_shut_down_p_nom_variable_upper: + description: "`Process-shut_down-p_nom-variable-upper` — a modular process stops only where a module is built" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_shut_down <= Process_n_mod +``` + +```math +\mathit{dn}^{z}_{\xi,t,j} \le N^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + ### `Line-fix-s-lower` `Line_fix_s_lower` @@ -4377,15 +4992,20 @@ Link_p_ramp_limit_down: Process_p_ramp_limit_up: description: >- `Process-p-ramp_limit_up` — a process raises internal power no faster than - its limit of the build. The translated term vacates the first snapshot, - where a plain optimize builds no row either + its ramp limit of the build, and a committed one no further than its + start-up ramp in the snapshot it turns on. A process that came into the + horizon running brought an unknown internal power, so it carries no row at the + first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] - where: Process_ramp_limit_up AND Process_active - expression: Process_p - shift(Process_p, along=snapshot, offset=1) <= Process_ramp_limit_up * Process_p_nom_effective + where: >- + Process_ramp_limit_up + AND NOT (Process_committable AND Process_p_nom_extendable) + AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + expression: Process_p - Process_previous_p <= Process_ramp_up_allowance ``` ```math -z_{\xi,t,j} - z_{\xi,t - 1,j} \le \mathrm{ru}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down` @@ -4394,14 +5014,22 @@ z_{\xi,t,j} - z_{\xi,t - 1,j} \le \mathrm{ru}^{z}_{j} \cdot \widetilde{\mathrm{z ```yaml Process_p_ramp_limit_down: - description: "`Process-p-ramp_limit_down` — a process lowers internal power no faster than its limit of the build" + description: >- + `Process-p-ramp_limit_down` — a process lowers internal power no faster than + its ramp limit of the build, and a committed one no further than its + shut-down ramp in the snapshot it turns off. A process that came into the + horizon running brought an unknown internal power, so it carries no row at the + first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] - where: Process_ramp_limit_down AND Process_active - expression: shift(Process_p, along=snapshot, offset=1) - Process_p <= Process_ramp_limit_down * Process_p_nom_effective + where: >- + Process_ramp_limit_down + AND NOT (Process_committable AND Process_p_nom_extendable) + AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + expression: Process_previous_p - Process_p <= Process_ramp_down_allowance ``` ```math -z_{\xi,t - 1,j} - z_{\xi,t,j} \le \mathrm{rd}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `StorageUnit-ext-p_dispatch-lower` @@ -5340,6 +5968,87 @@ Process_p_nom_effective: \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} = \begin{cases} Z_{j} & \text{if } \mathrm{ext}^{z}_{j} \\ \mathrm{z}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} ``` +### `Process_previous_status` + +```yaml +Process_previous_status: + description: >- + the commitment state a process carries into a snapshot — the state it + brought into the horizon at the first, the previous snapshot's after that + dims: [scenario, snapshot, process] + cases: + opening: { when: "position(snapshot) == 0", expression: Process_status_initial } + otherwise: shift(Process_status, along=snapshot, offset=1) +``` + +```math +\overleftarrow{u}^{z}_{\xi,t,j} = \begin{cases} \mathrm{u}^{z,0}_{j} & \text{if } \mathrm{pos}(t) = 0 \\ u^{z}_{\xi,t - 1,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +``` + +### `Process_previous_p` + +```yaml +Process_previous_p: + description: >- + the internal power a process carries into a snapshot — nothing at the start of + the horizon, which is why a process that came in running carries no ramp row + there + dims: [scenario, snapshot, process] + cases: + opening: { when: "position(snapshot) == 0", expression: 0 } + otherwise: shift(Process_p, along=snapshot, offset=1) +``` + +```math +\overleftarrow{z}_{\xi,t,j} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ z_{\xi,t - 1,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +``` + +### `Process_ramp_up_allowance` + +```yaml +Process_ramp_up_allowance: + description: >- + how far a process may raise internal power between two snapshots — its ramp + limit of the build while it stays on, plus its start-up ramp in the + snapshot it turns on + dims: [scenario, snapshot, process] + cases: + committed: + when: Process_committable + expression: >- + Process_ramp_limit_up * Process_p_nom * Process_previous_status + + Process_ramp_limit_start_up * Process_p_nom + * (Process_status - Process_previous_status) + otherwise: Process_ramp_limit_up * Process_p_nom_effective +``` + +```math +\Delta^{z,+}_{\xi,t,j} = \begin{cases} \mathrm{ru}^{z}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} + \mathrm{ru}^{z,\mathrm{up}}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \mathrm{ru}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +``` + +### `Process_ramp_down_allowance` + +```yaml +Process_ramp_down_allowance: + description: >- + how far a process may lower internal power between two snapshots — its ramp + limit of the build while it stays on, plus its shut-down ramp in the + snapshot it turns off + dims: [scenario, snapshot, process] + cases: + committed: + when: Process_committable + expression: >- + Process_ramp_limit_down * Process_p_nom * Process_status + + Process_ramp_limit_shut_down * Process_p_nom + * (Process_previous_status - Process_status) + otherwise: Process_ramp_limit_down * Process_p_nom_effective +``` + +```math +\Delta^{z,-}_{\xi,t,j} = \begin{cases} \mathrm{rd}^{z}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} + \mathrm{rd}^{z,\mathrm{dn}}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \mathrm{rd}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +``` + ### `StorageUnit_charge_carried_in` ```yaml @@ -5537,10 +6246,13 @@ scenario_opex: + sum(sum(Link_status * Link_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Link_start_up * Link_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Link_shut_down * Link_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Process_status * Process_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_start_up * Process_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_shut_down * Process_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) ``` ```math -\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Carrier_additions` @@ -5670,6 +6382,30 @@ u^{f}_{\xi,t,l} \ge 0, u^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \ \mathit{dn}^{f}_{\xi,t,l} \ge 0, \mathit{dn}^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` +**`Process_n_mod`** + +```math +N^{z}_{j} \ge 0, N^{z}_{j} \in \mathbb{Z} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 +``` + +**`Process_status`** + +```math +u^{z}_{\xi,t,j} \ge 0, u^{z}_{\xi,t,j} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +**`Process_start_up`** + +```math +\mathit{up}^{z}_{\xi,t,j} \ge 0, \mathit{up}^{z}_{\xi,t,j} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +**`Process_shut_down`** + +```math +\mathit{dn}^{z}_{\xi,t,j} \ge 0, \mathit{dn}^{z}_{\xi,t,j} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + **`Line_s`** ```math diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 3e9846a4..5f8490f3 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -324,6 +324,70 @@ parameters: Process_p_nom_set: description: a given nominal power for an extendable process; one without a value has no row here dims: [process] + Process_committable: + description: whether internal power is gated by an on/off status decision + dims: [process] + dtype: bool + Process_ramp_limit_start_up: + description: most internal power in the snapshot a process starts, per unit of nominal power + dims: [process] + Process_ramp_limit_shut_down: + description: most internal power in the snapshot before a process stops, per unit of nominal power + dims: [process] + Process_min_up_time: + description: least snapshots a process stays on once started + dims: [process] + dtype: int + Process_min_down_time: + description: least snapshots a process stays off once stopped + dims: [process] + dtype: int + Process_status_initial: + description: one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep + dims: [process] + dtype: int + Process_must_stay_up: + description: >- + true while the up time a process brought into the horizon still binds — + data prep, since `position()` compares against a literal rather than a + parameter + dims: [snapshot, process] + dtype: bool + Process_must_stay_down: + description: >- + true while the down time a process brought into the horizon still binds — + PyPSA's `min_down_time - down_time_before` snapshots, where + `down_time_before > 0`, data prep for the same reason + dims: [snapshot, process] + dtype: bool + Process_start_up_cost: + description: cost of one start + dims: [process] + Process_shut_down_cost: + description: cost of one stop + dims: [process] + Process_stand_by_cost: + description: cost of one snapshot spent on + dims: [snapshot, process] + Process_p_nom_mod: + description: the module size a build comes in whole numbers of; no value means the build is continuous + dims: [process] + Process_modules_installed: + description: >- + how many whole modules a committable build has in place: `Process_p_nom + / Process_p_nom_mod` where a fixed build is modular, one where it is + not, data prep. PyPSA refuses a fixed modular build whose nominal power + is not a whole number of modules + dims: [process] + Process_big_m: + description: a bound safely above any feasible internal power — the build cap at full availability, data prep + dims: [process] + Process_p_min_pu_nonneg: + description: >- + true where none of the process's own minimums-per-unit is negative — + PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep + dims: [process] + dtype: bool Load_p_set: description: demand dims: [scenario, snapshot, load] @@ -975,6 +1039,37 @@ variables: domain: integer bounds: lower: 0 + Process_n_mod: + description: "`Process-n_mod` — how many modules of an extendable modular build" + dims: [process] + where: Process_p_nom_extendable AND Process_p_nom_mod > 0 + domain: integer + bounds: + lower: 0 + Process_status: + description: >- + `Process-status` — how much of a committable process is on: an integer + the rows below cap at one, or at the module count where the build is + modular + dims: [scenario, snapshot, process] + where: Process_committable AND Process_active + domain: integer + bounds: + lower: 0 + Process_start_up: + description: "`Process-start_up` — how much of a committable process turns on this snapshot, capped as the status is" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_active + domain: integer + bounds: + lower: 0 + Process_shut_down: + description: "`Process-shut_down` — how much of a committable process turns off this snapshot, capped as the status is" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_active + domain: integer + bounds: + lower: 0 Line_s: description: >- `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a @@ -1183,6 +1278,51 @@ expressions: cases: extendable: { when: Process_p_nom_extendable, expression: Process_p_nom_ext } otherwise: Process_p_nom + Process_previous_status: + description: >- + the commitment state a process carries into a snapshot — the state it + brought into the horizon at the first, the previous snapshot's after that + dims: [scenario, snapshot, process] + cases: + opening: { when: "position(snapshot) == 0", expression: Process_status_initial } + otherwise: shift(Process_status, along=snapshot, offset=1) + Process_previous_p: + description: >- + the internal power a process carries into a snapshot — nothing at the start of + the horizon, which is why a process that came in running carries no ramp row + there + dims: [scenario, snapshot, process] + cases: + opening: { when: "position(snapshot) == 0", expression: 0 } + otherwise: shift(Process_p, along=snapshot, offset=1) + Process_ramp_up_allowance: + description: >- + how far a process may raise internal power between two snapshots — its ramp + limit of the build while it stays on, plus its start-up ramp in the + snapshot it turns on + dims: [scenario, snapshot, process] + cases: + committed: + when: Process_committable + expression: >- + Process_ramp_limit_up * Process_p_nom * Process_previous_status + + Process_ramp_limit_start_up * Process_p_nom + * (Process_status - Process_previous_status) + otherwise: Process_ramp_limit_up * Process_p_nom_effective + Process_ramp_down_allowance: + description: >- + how far a process may lower internal power between two snapshots — its ramp + limit of the build while it stays on, plus its shut-down ramp in the + snapshot it turns off + dims: [scenario, snapshot, process] + cases: + committed: + when: Process_committable + expression: >- + Process_ramp_limit_down * Process_p_nom * Process_status + + Process_ramp_limit_shut_down * Process_p_nom + * (Process_previous_status - Process_status) + otherwise: Process_ramp_limit_down * Process_p_nom_effective StorageUnit_charge_carried_in: description: >- the charge a unit opens a snapshot with — its last snapshot's less @@ -1296,6 +1436,9 @@ expressions: + sum(sum(Link_status * Link_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Link_start_up * Link_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Link_shut_down * Link_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Process_status * Process_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_start_up * Process_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_shut_down * Process_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) Carrier_additions: description: >- what a carrier adds in a period — every extendable component of that @@ -1395,22 +1538,22 @@ constraints: Process_fix_p_lower: description: "`Process-fix-p-lower` — a fixed process runs at least its minimum, negative for the other way" dims: [scenario, snapshot, process] - where: not Process_p_nom_extendable AND Process_active + where: not Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p >= Process_p_min_pu * Process_p_nom Process_fix_p_upper: description: "`Process-fix-p-upper` — a fixed process runs at most its nominal power" dims: [scenario, snapshot, process] - where: not Process_p_nom_extendable AND Process_active + where: not Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p <= Process_p_max_pu * Process_p_nom Process_ext_p_lower: description: "`Process-ext-p-lower` — an extendable process runs at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, process] - where: Process_p_nom_extendable AND Process_active + where: Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p >= Process_p_min_pu * Process_p_nom_ext Process_ext_p_upper: description: "`Process-ext-p-upper` — an extendable process runs at most the chosen build" dims: [scenario, snapshot, process] - where: Process_p_nom_extendable AND Process_active + where: Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p <= Process_p_max_pu * Process_p_nom_ext Process_ext_p_nom_lower: description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor" @@ -1839,6 +1982,196 @@ constraints: dims: [scenario, snapshot, link] where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active expression: Link_shut_down <= Link_n_mod + Process_com_p_lower: + description: "`Process-com-p-lower` — a committed process runs at least its minimum; off, at least nothing" + dims: [scenario, snapshot, process] + where: Process_committable AND not Process_p_nom_extendable AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom * Process_status + Process_com_p_upper: + description: "`Process-com-p-upper` — a committed process runs at most what is available; off, at most nothing" + dims: [scenario, snapshot, process] + where: Process_committable AND not Process_p_nom_extendable AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom * Process_status + Process_com_transition_start_up: + description: "`Process-com-transition-start-up` — turning on is a start, counted against the state the process carried into the snapshot" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_active + expression: Process_start_up >= Process_status - Process_previous_status + Process_com_transition_shut_down: + description: "`Process-com-transition-shut-down` — turning off is a stop, counted against the state the process carried into the snapshot" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_active + expression: Process_shut_down >= Process_previous_status - Process_status + Process_com_up_time: + description: >- + `Process-com-up-time` — a process started within its own minimum up time + is still on. The first snapshot's share of the window is the brought-in + up time's, which the must-stay-up mask carries + dims: [scenario, snapshot, process] + where: Process_committable AND Process_min_up_time > 0 AND position(snapshot) > 0 AND Process_active + expression: sum_back(Process_start_up, along=snapshot, window=Process_min_up_time) <= Process_status + Process_com_down_time: + description: >- + `Process-com-down-time` — a process stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries + dims: [scenario, snapshot, process] + where: Process_committable AND Process_min_down_time > 0 AND position(snapshot) > 0 AND Process_active + expression: sum_back(Process_shut_down, along=snapshot, window=Process_min_down_time) <= 1 - Process_status + Process_com_status_must_stay_up: + description: "`Process-com-status-min_up_time_must_stay_up` — a process still serving the up time it brought in stays on" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_must_stay_up AND Process_active + expression: Process_status == 1 + Process_com_status_must_stay_down: + description: >- + `Process-com-status-min_down_time_must_stay_up` — a process still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [scenario, snapshot, process] + where: Process_committable AND Process_must_stay_down AND Process_active + expression: Process_status == 0 + Process_p_ramp_limit_up_run_big_m: + description: >- + `Process-p-ramp_limit_up-run-bigM` — a committed extendable process + raises internal power no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns on + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_p - Process_previous_p <= + Process_ramp_limit_up * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_previous_status + Process_p_ramp_limit_up_start_big_m: + description: >- + `Process-p-ramp_limit_up-start-bigM` — in the snapshot it turns on, a + committed extendable process ramps no further than its start-up ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_p - Process_previous_p <= + Process_ramp_limit_start_up * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_start_up + Process_p_ramp_limit_down_run_big_m: + description: >- + `Process-p-ramp_limit_down-run-bigM` — a committed extendable process + lowers internal power no faster than its limit of the chosen build; the big M + releases the row in the snapshot it turns off + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_previous_p - Process_p <= + Process_ramp_limit_down * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_status + Process_p_ramp_limit_down_shut_big_m: + description: >- + `Process-p-ramp_limit_down-shut-bigM` — in the snapshot it turns off, + a committed extendable process ramps no further than its shut-down ramp of + the chosen build; the big M releases the row everywhere else + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + expression: >- + Process_previous_p - Process_p <= + Process_ramp_limit_shut_down * Process_p_nom_ext + + Process_big_m - Process_big_m * Process_shut_down + Process_p_nom_modularity: + description: "`Process-p_nom_modularity` — the chosen build is a whole number of modules" + dims: [process] + where: Process_p_nom_extendable AND Process_p_nom_mod > 0 + expression: Process_p_nom_ext == Process_p_nom_mod * Process_n_mod + Process_com_ext_p_upper_cap: + description: >- + `Process-com-ext-p-upper-cap` — a committed extendable process runs + at most what is available of the chosen build, whatever its status + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom_ext + Process_com_ext_p_upper_big_m: + description: "`Process-com-ext-p-upper-bigM` — off, a process does not run; on, the big M is no bound" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: Process_p <= Process_big_m * Process_status + Process_com_ext_p_lower: + description: >- + `Process-com-ext-p-lower` — a committed extendable process runs at + least its minimum of the chosen build; off, the big M releases the row + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: >- + Process_p >= + Process_p_min_pu * Process_p_nom_ext + + Process_big_m * Process_status - Process_big_m + Process_com_ext_p_lower_nonneg: + description: >- + `Process-com-ext-p-lower-nonneg` — where no minimum-per-unit is + negative, internal power is also plainly non-negative, a row the big-M lower + cannot assert while the process is off + dims: [scenario, snapshot, process] + where: >- + Process_committable AND Process_p_nom_extendable + AND Process_p_min_pu_nonneg AND NOT (Process_p_nom_mod > 0) AND Process_active + expression: Process_p >= 0 + Process_com_mod_p_lower: + description: >- + `Process-com-mod-p-lower` — a committed modular process runs at least + its minimum of one module, whether the build is fixed or a decision + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_p >= Process_p_min_pu * Process_p_nom_mod * Process_status + Process_com_mod_p_upper: + description: >- + `Process-com-mod-p-upper` — a committed modular process runs at most + one module's share, whether the build is fixed or a decision + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_p <= Process_p_max_pu * Process_p_nom_mod * Process_status + Process_status_p_fixed_upper: + description: >- + `Process-status-p-fixed-upper` — a status is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, process] + where: Process_committable AND NOT (Process_p_nom_extendable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_status <= Process_modules_installed + Process_start_up_p_fixed_upper: + description: >- + `Process-start_up-p-fixed-upper` — a start is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, process] + where: Process_committable AND NOT (Process_p_nom_extendable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_start_up <= Process_modules_installed + Process_shut_down_p_fixed_upper: + description: >- + `Process-shut_down-p-fixed-upper` — a stop is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, process] + where: Process_committable AND NOT (Process_p_nom_extendable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_shut_down <= Process_modules_installed + Process_status_p_nom_variable_upper: + description: "`Process-status-p_nom-variable-upper` — a modular process is on only where a module is built" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_status <= Process_n_mod + Process_start_up_p_nom_variable_upper: + description: "`Process-start_up-p_nom-variable-upper` — a modular process starts only where a module is built" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_start_up <= Process_n_mod + Process_shut_down_p_nom_variable_upper: + description: "`Process-shut_down-p_nom-variable-upper` — a modular process stops only where a module is built" + dims: [scenario, snapshot, process] + where: Process_committable AND Process_p_nom_extendable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_shut_down <= Process_n_mod Line_fix_s_lower: description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating, the loss counted against it" dims: [scenario, snapshot, line] @@ -2029,16 +2362,29 @@ constraints: Process_p_ramp_limit_up: description: >- `Process-p-ramp_limit_up` — a process raises internal power no faster than - its limit of the build. The translated term vacates the first snapshot, - where a plain optimize builds no row either + its ramp limit of the build, and a committed one no further than its + start-up ramp in the snapshot it turns on. A process that came into the + horizon running brought an unknown internal power, so it carries no row at the + first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] - where: Process_ramp_limit_up AND Process_active - expression: Process_p - shift(Process_p, along=snapshot, offset=1) <= Process_ramp_limit_up * Process_p_nom_effective + where: >- + Process_ramp_limit_up + AND NOT (Process_committable AND Process_p_nom_extendable) + AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + expression: Process_p - Process_previous_p <= Process_ramp_up_allowance Process_p_ramp_limit_down: - description: "`Process-p-ramp_limit_down` — a process lowers internal power no faster than its limit of the build" + description: >- + `Process-p-ramp_limit_down` — a process lowers internal power no faster than + its ramp limit of the build, and a committed one no further than its + shut-down ramp in the snapshot it turns off. A process that came into the + horizon running brought an unknown internal power, so it carries no row at the + first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] - where: Process_ramp_limit_down AND Process_active - expression: shift(Process_p, along=snapshot, offset=1) - Process_p <= Process_ramp_limit_down * Process_p_nom_effective + where: >- + Process_ramp_limit_down + AND NOT (Process_committable AND Process_p_nom_extendable) + AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + expression: Process_previous_p - Process_p <= Process_ramp_down_allowance StorageUnit_ext_p_dispatch_lower: description: "`StorageUnit-ext-p_dispatch-lower` — dispatch is non-negative" dims: [scenario, snapshot, storage_unit] diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 84716cc5..e464a0c0 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -899,5 +899,57 @@ "Link-status-p-fixed-upper": 16, "Link-status-p_nom-variable-upper": 4 } + }, + "rung_26_committable_process": { + "columns": { + "Generator-p": 8, + "Link-p": 4, + "Process-n_mod": 1, + "Process-p": 20, + "Process-p_nom": 2, + "Process-shut_down": 20, + "Process-start_up": 20, + "Process-status": 20 + }, + "global_constraints": {}, + "objective": 15956.125, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Process-com-down-time": 6, + "Process-com-ext-p-lower": 4, + "Process-com-ext-p-lower-nonneg": 4, + "Process-com-ext-p-upper-bigM": 4, + "Process-com-ext-p-upper-cap": 4, + "Process-com-mod-p-lower": 8, + "Process-com-mod-p-upper": 8, + "Process-com-p-lower": 12, + "Process-com-p-upper": 12, + "Process-com-status-min_down_time_must_stay_up": 2, + "Process-com-status-min_up_time_must_stay_up": 2, + "Process-com-transition-shut-down": 20, + "Process-com-transition-start-up": 20, + "Process-com-up-time": 6, + "Process-ext-p_nom-lower": 2, + "Process-ext-p_nom-upper": 2, + "Process-p-ramp_limit_down": 3, + "Process-p-ramp_limit_down-run-bigM": 4, + "Process-p-ramp_limit_down-shut-bigM": 4, + "Process-p-ramp_limit_up": 3, + "Process-p-ramp_limit_up-run-bigM": 4, + "Process-p-ramp_limit_up-start-bigM": 4, + "Process-p_nom_modularity": 1, + "Process-shut_down-p-fixed-upper": 16, + "Process-shut_down-p_nom-variable-upper": 4, + "Process-start_up-p-fixed-upper": 16, + "Process-start_up-p_nom-variable-upper": 4, + "Process-status-p-fixed-upper": 16, + "Process-status-p_nom-variable-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_26_committable_process.py b/examples/references/pypsa/rung_26_committable_process.py new file mode 100644 index 00000000..bfb9cdf9 --- /dev/null +++ b/examples/references/pypsa/rung_26_committable_process.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 26: committable processes — rung 25's committable links restated as processes that draw a quarter more than they deliver.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus that only committable processes serve.""" + n = spine.build() + n.add('Bus', 'east') + n.add( + 'Process', + 'warm_conv', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom=60, + p_min_pu=0.3, + marginal_cost=8, + min_up_time=3, + min_down_time=2, + up_time_before=1, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ramp_limit_start_up=0.6, + ramp_limit_shut_down=0.6, + start_up_cost=100, + shut_down_cost=50, + stand_by_cost=5, + ) + n.add( + 'Process', + 'cold_conv', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom=40, + p_min_pu=0.2, + min_up_time=2, + min_down_time=3, + up_time_before=0, + down_time_before=1, + start_up_cost=20, + ) + n.add( + 'Process', + 'ext_conv', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom_extendable=True, + p_nom_max=30, + capital_cost=5, + p_min_pu=0.2, + marginal_cost=2, + up_time_before=0, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + ) + n.add( + 'Process', + 'mod_conv', + bus0='south', + bus1='east', + rate0=-1.25, + committable=True, + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=40, + capital_cost=3, + p_min_pu=0.5, + ) + n.add( + 'Process', + 'mod_fix', + bus0='north', + bus1='east', + rate0=-1.25, + committable=True, + p_nom=20, + p_nom_mod=10, + p_min_pu=0.5, + marginal_cost=1, + ) + n.add('Load', 'east_load', bus='east', p_set=[20, 70, 60, 5]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 7afb5e96..5705fda4 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -130,6 +130,29 @@ names: Process_p_nom_ext: "Z" Process_p_nom_effective: '\widetilde{\mathrm{z}}^{\mathrm{nom}}' Process_output_arrival: '\overrightarrow{z}' + Process_committable: '\mathrm{com}^{z}' + Process_ramp_limit_start_up: '\mathrm{ru}^{z,\mathrm{up}}' + Process_ramp_limit_shut_down: '\mathrm{rd}^{z,\mathrm{dn}}' + Process_status: "u^{z}" + Process_start_up: '\mathit{up}^{z}' + Process_shut_down: '\mathit{dn}^{z}' + Process_min_up_time: '\mathrm{UT}^{z}' + Process_min_down_time: '\mathrm{DT}^{z}' + Process_status_initial: '\mathrm{u}^{z,0}' + Process_previous_status: '\overleftarrow{u}^{z}' + Process_previous_p: '\overleftarrow{z}' + Process_ramp_up_allowance: '\Delta^{z,+}' + Process_ramp_down_allowance: '\Delta^{z,-}' + Process_must_stay_up: '\mathrm{hold}^{z}' + Process_must_stay_down: '\mathrm{rest}^{z}' + Process_start_up_cost: '\mathrm{c}^{z,\mathrm{up}}' + Process_shut_down_cost: '\mathrm{c}^{z,\mathrm{dn}}' + Process_stand_by_cost: '\mathrm{c}^{z,\mathrm{on}}' + Process_p_nom_mod: '\mathrm{z}^{\mathrm{mod}}' + Process_n_mod: "N^{z}" + Process_modules_installed: '\mathrm{N}^{z,\mathrm{fix}}' + Process_big_m: '\mathrm{M}^{z}' + Process_p_min_pu_nonneg: '\mathrm{nonneg}^{z}' StorageUnit_p_nom_ext: "H" StorageUnit_p_nom_min: '\underline{\mathrm{h}}^{\mathrm{nom}}' StorageUnit_p_nom_max: '\overline{\mathrm{h}}^{\mathrm{nom}}' diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index 88e01053..b34c500e 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -270,6 +270,48 @@ constraints: Link_status_p_nom_variable_upper: - snapshot - link + Process_com_down_time: + - snapshot + - process + Process_com_ext_p_lower: + - snapshot + - process + Process_com_ext_p_lower_nonneg: + - snapshot + - process + Process_com_ext_p_upper_big_m: + - snapshot + - process + Process_com_ext_p_upper_cap: + - snapshot + - process + Process_com_mod_p_lower: + - snapshot + - process + Process_com_mod_p_upper: + - snapshot + - process + Process_com_p_lower: + - snapshot + - process + Process_com_p_upper: + - snapshot + - process + Process_com_status_must_stay_down: + - snapshot + - process + Process_com_status_must_stay_up: + - snapshot + - process + Process_com_transition_shut_down: + - snapshot + - process + Process_com_transition_start_up: + - snapshot + - process + Process_com_up_time: + - snapshot + - process Process_ext_p_lower: - snapshot - process @@ -286,17 +328,49 @@ constraints: Process_fix_p_upper: - snapshot - process + Process_p_nom_modularity: + - process Process_p_nom_set: - process Process_p_ramp_limit_down: - snapshot - process + Process_p_ramp_limit_down_run_big_m: + - snapshot + - process + Process_p_ramp_limit_down_shut_big_m: + - snapshot + - process Process_p_ramp_limit_up: - snapshot - process + Process_p_ramp_limit_up_run_big_m: + - snapshot + - process + Process_p_ramp_limit_up_start_big_m: + - snapshot + - process Process_p_set: - snapshot - process + Process_shut_down_p_fixed_upper: + - snapshot + - process + Process_shut_down_p_nom_variable_upper: + - snapshot + - process + Process_start_up_p_fixed_upper: + - snapshot + - process + Process_start_up_p_nom_variable_upper: + - snapshot + - process + Process_status_p_fixed_upper: + - snapshot + - process + Process_status_p_nom_variable_upper: + - snapshot + - process StorageUnit_energy_balance: - snapshot - storage_unit @@ -407,6 +481,10 @@ expressions: - Link_ramp_up_allowance - Process_output_arrival - Process_p_nom_effective + - Process_previous_p + - Process_previous_status + - Process_ramp_down_allowance + - Process_ramp_up_allowance - StorageUnit_charge_carried_in - Store_energy_carried_in - operational_limit @@ -624,13 +702,29 @@ parameters: Process_active: - snapshot - process + Process_big_m: + - process Process_capital_cost: - process Process_capital_weight: - process + Process_committable: + - process Process_marginal_cost: - snapshot - process + Process_min_down_time: + - process + Process_min_up_time: + - process + Process_modules_installed: + - process + Process_must_stay_down: + - snapshot + - process + Process_must_stay_up: + - snapshot + - process Process_output_cyclic_delay: - process_output Process_output_delay: @@ -641,6 +735,8 @@ parameters: Process_p_min_pu: - snapshot - process + Process_p_min_pu_nonneg: + - process Process_p_nom: - process Process_p_nom_extendable: @@ -649,6 +745,8 @@ parameters: - process Process_p_nom_min: - process + Process_p_nom_mod: + - process Process_p_nom_set: - process Process_p_set: @@ -656,10 +754,23 @@ parameters: - process Process_ramp_limit_down: - process + Process_ramp_limit_shut_down: + - process + Process_ramp_limit_start_up: + - process Process_ramp_limit_up: - process Process_rate: - process_output + Process_shut_down_cost: + - process + Process_stand_by_cost: + - snapshot + - process + Process_start_up_cost: + - process + Process_status_initial: + - process Process_tech_capacity_weight: - global_constraint - process @@ -874,11 +985,22 @@ variables: Link_status: - snapshot - link + Process_n_mod: + - process Process_p: - snapshot - process Process_p_nom_ext: - process + Process_shut_down: + - snapshot + - process + Process_start_up: + - snapshot + - process + Process_status: + - snapshot + - process StorageUnit_p_dispatch: - snapshot - storage_unit diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index 452e9687..344a8561 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -28,6 +28,7 @@ SCENARIO_FREE_VARIABLES = { 'Generator_n_mod', 'Link_n_mod', + 'Process_n_mod', 'Generator_p_nom_ext', 'Link_p_nom_ext', 'StorageUnit_p_nom_ext', From 4f67f5359be8f7d9a97dcfe83edc6a8121b883c9 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 10:43:38 +0200 Subject: [PATCH 13/46] 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/examples/pypsa.md | 561 +++++++++++++++--- examples/pypsa.yaml | 240 ++++++-- examples/references/pypsa/references.json | 123 ++++ .../references/pypsa/rung_27_modular_ramp.py | 34 ++ .../references/pypsa/rung_28_start_up_ramp.py | 29 + examples/symbols/pypsa.yaml | 15 + 6 files changed, 868 insertions(+), 134 deletions(-) create mode 100644 examples/references/pypsa/rung_27_modular_ramp.py create mode 100644 examples/references/pypsa/rung_28_start_up_ramp.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 10c50503..f22c2b8f 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -374,7 +374,7 @@ def build(): | PyPSA | status | note | | ------------------------------ | ------ | ---------------------------------------------------------- | -| [`{c}-p-ramp_limit_up/down`](#generator-p-ramp_limit_up) | done | the build, the allowance and the output carried in are cased quantities, so fixed, extendable and committed are one block; big-M is rung 8's | +| [`{c}-p-ramp_limit_up/down`](#generator-p-ramp_limit_up) | done | the build, the allowance and the output carried in are cased quantities, so fixed, extendable and committed are one block; big-M is rung 8's. A missing limit reads as the full build, and a start-up or shut-down ramp alone builds the row, rung 28 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `8785.0`, 64 rows. @@ -676,7 +676,7 @@ def build(): | [`{c}-com-mod-p-lower/upper`](#generator-com-mod-p-lower) | done | one module's share, times the status — a fixed build too, beside its ordinary `com-p-*` rows | | [`{c}-com-ext-p-*` (big-M)](#generator-com-ext-p-upper-cap) | done | a cap row beside a big-M row; `M` is the build cap at full availability, data prep | | [`{c}-com-ext-p-lower-nonneg`](#generator-com-ext-p-lower-nonneg) | done | `(p_min_pu >= 0).all()` is prep | -| [`{c}-p-ramp_limit_*-bigM`](#generator-p-ramp_limit_up-run-bigm) | done | run and start rows up, run and shut rows down; the output carried in is a cased quantity, so each is one block | +| [`{c}-p-ramp_limit_*-bigM`](#generator-p-ramp_limit_up-run-bigm) | done | run and start rows up, run and shut rows down; the output carried in is a cased quantity, so each is one block. A modular build takes the ordinary rows against one module instead, rung 27 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `15915.0`, 191 rows. @@ -2278,6 +2278,125 @@ def build(): +### Rung 27 — modular ramps + +A committable, extendable and modular unit gets no big-M ramp rows. PyPSA gives +it the ordinary `{c}-p-ramp_limit_*` rows of a committed unit, with one module +`p_nom_mod` in place of `p_nom`. The status counts the modules that are on, so +the allowance grows with each module. This rung has one such generator, link +and process on a peak bus, each with a ramp limit of one half and a start-up +and shut-down ramp of 0.6. The ramp rows bind at the rise and at the fall of +the load. + +| PyPSA | status | note | +| --- | --- | --- | +| [`{c}-p-ramp_limit_up/down`, modular](#generator-p-ramp_limit_up) | done | the committed allowance reads `p_nom_committed`: one module where the build is extendable and modular, `p_nom` otherwise | +| [`{c}-p-ramp_limit_*-bigM`, modular](#generator-p-ramp_limit_up-run-bigm) | done | not built for a modular build | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `45469.49999999998`, 161 rows. + +
+The network, as PyPSA code + +`rung_27_modular_ramp.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 27: modular ramps — a committable extendable modular unit ramps against one module through the ordinary ramp rows, not the big-M ones.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a peak bus served by a committable modular generator, link and process, each ramp-limited, with a dear backup.""" + n = spine.build() + n.add('Bus', 'peak') + common = { + 'committable': True, + 'p_nom_extendable': True, + 'p_nom_mod': 20, + 'p_nom_max': 60, + 'capital_cost': 2, + 'p_min_pu': 0.2, + 'up_time_before': 0, + 'ramp_limit_up': 0.5, + 'ramp_limit_down': 0.5, + 'ramp_limit_start_up': 0.6, + 'ramp_limit_shut_down': 0.6, + } + n.add('Generator', 'mod_gen', bus='peak', marginal_cost=3, **common) + n.add('Link', 'mod_link', bus0='north', bus1='peak', marginal_cost=4, **common) + n.add('Process', 'mod_proc', bus0='south', bus1='peak', rate0=-1.25, marginal_cost=5, **common) + n.add('Generator', 'peak_backup', bus='peak', p_nom=200, marginal_cost=500) + n.add('Load', 'peak_load', bus='peak', p_set=[10, 90, 150, 20]) + return n +``` + +
+ + +### Rung 28 — a start-up ramp alone + +PyPSA builds a ramp row where either the ramp limit or the start-up ramp is +given, and reads the missing one as `1.0`, the full build. The down row is the +same with the shut-down ramp. This rung has a committable generator, link and +process that carry only a start-up ramp of 0.4 and a shut-down ramp of 0.5. The +start-up ramp caps the snapshot each unit turns on, and the shut-down ramp caps +the snapshot before it turns off. + +| PyPSA | status | note | +| --- | --- | --- | +| [`{c}-p-ramp_limit_up/down`, start-up or shut-down ramp alone](#generator-p-ramp_limit_up) | done | the `where:` reads either limit; `ramp_up_rate` and its three siblings read a missing one as `1` | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `83282.99999999983`, 152 rows. + +
+The network, as PyPSA code + +`rung_28_start_up_ramp.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 28: a start-up ramp alone — a committable unit with only a start-up and a shut-down ramp still gets ramp rows, at the full build between them.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a pulse bus served by committable generator, link and process that carry only start-up and shut-down ramps, with a dear backup.""" + n = spine.build() + n.add('Bus', 'pulse') + common = { + 'committable': True, + 'p_nom': 40, + 'p_min_pu': 0.1, + 'up_time_before': 0, + 'ramp_limit_start_up': 0.4, + 'ramp_limit_shut_down': 0.5, + } + n.add('Generator', 'pulse_gen', bus='pulse', marginal_cost=3, **common) + n.add('Link', 'pulse_link', bus0='north', bus1='pulse', marginal_cost=4, **common) + n.add('Process', 'pulse_proc', bus0='south', bus1='pulse', rate0=-1.25, marginal_cost=5, **common) + n.add('Generator', 'pulse_backup', bus='pulse', p_nom=200, marginal_cost=500) + n.add('Load', 'pulse_load', bus='pulse', p_set=[0, 60, 110, 0]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2585,16 +2704,31 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | | $`\widetilde{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_effective` over $`\mathcal{G}`$ — the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise | +| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{\mathrm{up}}`$ | `Generator_start_up_rate` over $`\mathcal{G}`$ — the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | +| $`\widetilde{\mathrm{rd}}^{\mathrm{dn}}`$ | `Generator_shut_down_rate` over $`\mathcal{G}`$ — the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | +| $`\widehat{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_committed` over $`\mathcal{G}`$ — the build a committed unit's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{+}`$ | `Generator_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{f}`$ | `Link_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the commitment state a link carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{f}`$ | `Link_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the flow a link carries into a snapshot — nothing at the start of the horizon, which is why a link that came in running carries no ramp row there | +| $`\widetilde{\mathrm{ru}}^{f}`$ | `Link_ramp_up_rate` over $`\mathcal{L}`$ — the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}^{f}`$ | `Link_ramp_down_rate` over $`\mathcal{L}`$ — the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{f,\mathrm{up}}`$ | `Link_start_up_rate` over $`\mathcal{L}`$ — the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | +| $`\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}`$ | `Link_shut_down_rate` over $`\mathcal{L}`$ — the shut-down ramp a link's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | +| $`\widehat{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_committed` over $`\mathcal{L}`$ — the build a committed link's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{f,+}`$ | `Link_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may raise flow between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{f,-}`$ | `Link_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may lower flow between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{z}`$ | `Process_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the commitment state a process carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{z}`$ | `Process_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the internal power a process carries into a snapshot — nothing at the start of the horizon, which is why a process that came in running carries no ramp row there | +| $`\widetilde{\mathrm{ru}}^{z}`$ | `Process_ramp_up_rate` over $`\mathcal{J}`$ — the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}^{z}`$ | `Process_ramp_down_rate` over $`\mathcal{J}`$ — the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{z,\mathrm{up}}`$ | `Process_start_up_rate` over $`\mathcal{J}`$ — the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | +| $`\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}`$ | `Process_shut_down_rate` over $`\mathcal{J}`$ — the shut-down ramp a process's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | +| $`\widehat{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_committed` over $`\mathcal{J}`$ — the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{z,+}`$ | `Process_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may raise internal power between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{z,-}`$ | `Process_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may lower internal power between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | @@ -3253,16 +3387,17 @@ Generator_p_ramp_limit_up_run_big_m: releases the row in the snapshot it turns on dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= - Generator_ramp_limit_up * Generator_p_nom_ext + Generator_ramp_up_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_previous_status ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \mathrm{ru}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{ru}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-start-bigM` @@ -3277,16 +3412,17 @@ Generator_p_ramp_limit_up_start_big_m: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= - Generator_ramp_limit_start_up * Generator_p_nom_ext + Generator_start_up_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_start_up ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \mathrm{ru}^{\mathrm{up}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{ru}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-run-bigM` @@ -3301,16 +3437,17 @@ Generator_p_ramp_limit_down_run_big_m: releases the row in the snapshot it turns off dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= - Generator_ramp_limit_down * Generator_p_nom_ext + Generator_ramp_down_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_status ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \mathrm{rd}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{rd}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-shut-bigM` @@ -3325,16 +3462,17 @@ Generator_p_ramp_limit_down_shut_big_m: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= - Generator_ramp_limit_shut_down * Generator_p_nom_ext + Generator_shut_down_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_shut_down ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \mathrm{rd}^{\mathrm{dn}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{rd}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p_nom_modularity` @@ -3718,16 +3856,17 @@ Link_p_ramp_limit_up_run_big_m: releases the row in the snapshot it turns on dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_p - Link_previous_p <= - Link_ramp_limit_up * Link_p_nom_ext + Link_ramp_up_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_previous_status ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \mathrm{ru}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_up-start-bigM` @@ -3742,16 +3881,17 @@ Link_p_ramp_limit_up_start_big_m: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_p - Link_previous_p <= - Link_ramp_limit_start_up * Link_p_nom_ext + Link_start_up_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_start_up ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \mathrm{ru}^{f,\mathrm{up}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-run-bigM` @@ -3766,16 +3906,17 @@ Link_p_ramp_limit_down_run_big_m: releases the row in the snapshot it turns off dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_previous_p - Link_p <= - Link_ramp_limit_down * Link_p_nom_ext + Link_ramp_down_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_status ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \mathrm{rd}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-shut-bigM` @@ -3790,16 +3931,17 @@ Link_p_ramp_limit_down_shut_big_m: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_previous_p - Link_p <= - Link_ramp_limit_shut_down * Link_p_nom_ext + Link_shut_down_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_shut_down ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \mathrm{rd}^{f,\mathrm{dn}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p_nom_modularity` @@ -4183,16 +4325,17 @@ Process_p_ramp_limit_up_run_big_m: releases the row in the snapshot it turns on dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_p - Process_previous_p <= - Process_ramp_limit_up * Process_p_nom_ext + Process_ramp_up_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_previous_status ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \mathrm{ru}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_up-start-bigM` @@ -4207,16 +4350,17 @@ Process_p_ramp_limit_up_start_big_m: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_p - Process_previous_p <= - Process_ramp_limit_start_up * Process_p_nom_ext + Process_start_up_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_start_up ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \mathrm{ru}^{z,\mathrm{up}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-run-bigM` @@ -4231,16 +4375,17 @@ Process_p_ramp_limit_down_run_big_m: releases the row in the snapshot it turns off dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_previous_p - Process_p <= - Process_ramp_limit_down * Process_p_nom_ext + Process_ramp_down_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_status ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \mathrm{rd}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-shut-bigM` @@ -4255,16 +4400,17 @@ Process_p_ramp_limit_down_shut_big_m: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_previous_p - Process_p <= - Process_ramp_limit_shut_down * Process_p_nom_ext + Process_shut_down_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_shut_down ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \mathrm{rd}^{z,\mathrm{dn}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p_nom_modularity` @@ -4902,14 +5048,14 @@ Generator_p_ramp_limit_up: first snapshot — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- - Generator_ramp_limit_up - AND NOT (Generator_committable AND Generator_p_nom_extendable) + (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) + AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ru}_{g} \text{ is defined} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down` @@ -4926,14 +5072,14 @@ Generator_p_ramp_limit_down: first snapshot — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- - Generator_ramp_limit_down - AND NOT (Generator_committable AND Generator_p_nom_extendable) + (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) + AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{rd}_{g} \text{ is defined} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Link-p-ramp_limit_up` @@ -4950,14 +5096,14 @@ Link_p_ramp_limit_up: first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- - Link_ramp_limit_up - AND NOT (Link_committable AND Link_p_nom_extendable) + (Link_ramp_limit_up OR Link_ramp_limit_start_up) + AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active expression: Link_p - Link_previous_p <= Link_ramp_up_allowance ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ru}^{f}_{l} \text{ is defined} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down` @@ -4974,14 +5120,14 @@ Link_p_ramp_limit_down: first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- - Link_ramp_limit_down - AND NOT (Link_committable AND Link_p_nom_extendable) + (Link_ramp_limit_down OR Link_ramp_limit_shut_down) + AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active expression: Link_previous_p - Link_p <= Link_ramp_down_allowance ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{rd}^{f}_{l} \text{ is defined} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-p-ramp_limit_up` @@ -4998,14 +5144,14 @@ Process_p_ramp_limit_up: first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- - Process_ramp_limit_up - AND NOT (Process_committable AND Process_p_nom_extendable) + (Process_ramp_limit_up OR Process_ramp_limit_start_up) + AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active expression: Process_p - Process_previous_p <= Process_ramp_up_allowance ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ru}^{z}_{j} \text{ is defined} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down` @@ -5022,14 +5168,14 @@ Process_p_ramp_limit_down: first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- - Process_ramp_limit_down - AND NOT (Process_committable AND Process_p_nom_extendable) + (Process_ramp_limit_down OR Process_ramp_limit_shut_down) + AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active expression: Process_previous_p - Process_p <= Process_ramp_down_allowance ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{rd}^{z}_{j} \text{ is defined} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `StorageUnit-ext-p_dispatch-lower` @@ -5811,6 +5957,91 @@ Generator_p_nom_effective: \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} = \begin{cases} P_{g} & \text{if } \mathrm{ext}_{g} \\ \mathrm{p}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} ``` +### `Generator_ramp_up_rate` + +```yaml +Generator_ramp_up_rate: + description: >- + the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}_{g} = \begin{cases} \mathrm{ru}_{g} & \text{if } \mathrm{ru}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + +### `Generator_ramp_down_rate` + +```yaml +Generator_ramp_down_rate: + description: >- + the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}_{g} = \begin{cases} \mathrm{rd}_{g} & \text{if } \mathrm{rd}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + +### `Generator_start_up_rate` + +```yaml +Generator_start_up_rate: + description: >- + the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_start_up, expression: Generator_ramp_limit_start_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} = \begin{cases} \mathrm{ru}^{\mathrm{up}}_{g} & \text{if } \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + +### `Generator_shut_down_rate` + +```yaml +Generator_shut_down_rate: + description: >- + the shut-down ramp a unit's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_shut_down, expression: Generator_ramp_limit_shut_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} = \begin{cases} \mathrm{rd}^{\mathrm{dn}}_{g} & \text{if } \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + +### `Generator_p_nom_committed` + +```yaml +Generator_p_nom_committed: + description: >- + the build a committed unit's ramp rows are taken against — one module + where the build is extendable and modular, the given build otherwise + dims: [generator] + cases: + modular_build: { when: Generator_p_nom_extendable AND Generator_p_nom_mod > 0, expression: Generator_p_nom_mod } + otherwise: Generator_p_nom +``` + +```math +\widehat{\mathrm{p}}^{\mathrm{nom}}_{g} = \begin{cases} \mathrm{p}^{\mathrm{mod}}_{g} & \text{if } \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \\ \mathrm{p}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + ### `Generator_ramp_up_allowance` ```yaml @@ -5824,14 +6055,14 @@ Generator_ramp_up_allowance: committed: when: Generator_committable expression: >- - Generator_ramp_limit_up * Generator_p_nom * Generator_previous_status - + Generator_ramp_limit_start_up * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom_committed * Generator_previous_status + + Generator_start_up_rate * Generator_p_nom_committed * (Generator_status - Generator_previous_status) - otherwise: Generator_ramp_limit_up * Generator_p_nom_effective + otherwise: Generator_ramp_up_rate * Generator_p_nom_effective ``` ```math -\Delta^{+}_{\xi,t,g} = \begin{cases} \mathrm{ru}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{\xi,t,g} + \mathrm{ru}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \mathrm{ru}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{+}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{ru}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{\xi,t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{ru}}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_ramp_down_allowance` @@ -5847,14 +6078,14 @@ Generator_ramp_down_allowance: committed: when: Generator_committable expression: >- - Generator_ramp_limit_down * Generator_p_nom * Generator_status - + Generator_ramp_limit_shut_down * Generator_p_nom + Generator_ramp_down_rate * Generator_p_nom_committed * Generator_status + + Generator_shut_down_rate * Generator_p_nom_committed * (Generator_previous_status - Generator_status) - otherwise: Generator_ramp_limit_down * Generator_p_nom_effective + otherwise: Generator_ramp_down_rate * Generator_p_nom_effective ``` ```math -\Delta^{-}_{\xi,t,g} = \begin{cases} \mathrm{rd}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} + \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \mathrm{rd}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{-}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{rd}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{rd}}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Link_p_nom_effective` @@ -5907,6 +6138,91 @@ Link_previous_p: \overleftarrow{f}_{\xi,t,l} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ f_{\xi,t - 1,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` +### `Link_ramp_up_rate` + +```yaml +Link_ramp_up_rate: + description: >- + the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [link] + cases: + given: { when: Link_ramp_limit_up, expression: Link_ramp_limit_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}^{f}_{l} = \begin{cases} \mathrm{ru}^{f}_{l} & \text{if } \mathrm{ru}^{f}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +``` + +### `Link_ramp_down_rate` + +```yaml +Link_ramp_down_rate: + description: >- + the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [link] + cases: + given: { when: Link_ramp_limit_down, expression: Link_ramp_limit_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}^{f}_{l} = \begin{cases} \mathrm{rd}^{f}_{l} & \text{if } \mathrm{rd}^{f}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +``` + +### `Link_start_up_rate` + +```yaml +Link_start_up_rate: + description: >- + the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [link] + cases: + given: { when: Link_ramp_limit_start_up, expression: Link_ramp_limit_start_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} = \begin{cases} \mathrm{ru}^{f,\mathrm{up}}_{l} & \text{if } \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +``` + +### `Link_shut_down_rate` + +```yaml +Link_shut_down_rate: + description: >- + the shut-down ramp a link's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [link] + cases: + given: { when: Link_ramp_limit_shut_down, expression: Link_ramp_limit_shut_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} = \begin{cases} \mathrm{rd}^{f,\mathrm{dn}}_{l} & \text{if } \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +``` + +### `Link_p_nom_committed` + +```yaml +Link_p_nom_committed: + description: >- + the build a committed link's ramp rows are taken against — one module + where the build is extendable and modular, the given build otherwise + dims: [link] + cases: + modular_build: { when: Link_p_nom_extendable AND Link_p_nom_mod > 0, expression: Link_p_nom_mod } + otherwise: Link_p_nom +``` + +```math +\widehat{\mathrm{f}}^{\mathrm{nom}}_{l} = \begin{cases} \mathrm{f}^{\mathrm{mod}}_{l} & \text{if } \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \\ \mathrm{f}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +``` + ### `Link_ramp_up_allowance` ```yaml @@ -5920,14 +6236,14 @@ Link_ramp_up_allowance: committed: when: Link_committable expression: >- - Link_ramp_limit_up * Link_p_nom * Link_previous_status - + Link_ramp_limit_start_up * Link_p_nom + Link_ramp_up_rate * Link_p_nom_committed * Link_previous_status + + Link_start_up_rate * Link_p_nom_committed * (Link_status - Link_previous_status) - otherwise: Link_ramp_limit_up * Link_p_nom_effective + otherwise: Link_ramp_up_rate * Link_p_nom_effective ``` ```math -\Delta^{f,+}_{\xi,t,l} = \begin{cases} \mathrm{ru}^{f}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} + \mathrm{ru}^{f,\mathrm{up}}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \mathrm{ru}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\Delta^{f,+}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{ru}}^{f}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} + \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{ru}}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Link_ramp_down_allowance` @@ -5943,14 +6259,14 @@ Link_ramp_down_allowance: committed: when: Link_committable expression: >- - Link_ramp_limit_down * Link_p_nom * Link_status - + Link_ramp_limit_shut_down * Link_p_nom + Link_ramp_down_rate * Link_p_nom_committed * Link_status + + Link_shut_down_rate * Link_p_nom_committed * (Link_previous_status - Link_status) - otherwise: Link_ramp_limit_down * Link_p_nom_effective + otherwise: Link_ramp_down_rate * Link_p_nom_effective ``` ```math -\Delta^{f,-}_{\xi,t,l} = \begin{cases} \mathrm{rd}^{f}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} + \mathrm{rd}^{f,\mathrm{dn}}_{l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \mathrm{rd}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\Delta^{f,-}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{rd}}^{f}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} + \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{rd}}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Process_p_nom_effective` @@ -6003,6 +6319,91 @@ Process_previous_p: \overleftarrow{z}_{\xi,t,j} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ z_{\xi,t - 1,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` +### `Process_ramp_up_rate` + +```yaml +Process_ramp_up_rate: + description: >- + the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [process] + cases: + given: { when: Process_ramp_limit_up, expression: Process_ramp_limit_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}^{z}_{j} = \begin{cases} \mathrm{ru}^{z}_{j} & \text{if } \mathrm{ru}^{z}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +``` + +### `Process_ramp_down_rate` + +```yaml +Process_ramp_down_rate: + description: >- + the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [process] + cases: + given: { when: Process_ramp_limit_down, expression: Process_ramp_limit_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}^{z}_{j} = \begin{cases} \mathrm{rd}^{z}_{j} & \text{if } \mathrm{rd}^{z}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +``` + +### `Process_start_up_rate` + +```yaml +Process_start_up_rate: + description: >- + the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [process] + cases: + given: { when: Process_ramp_limit_start_up, expression: Process_ramp_limit_start_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} = \begin{cases} \mathrm{ru}^{z,\mathrm{up}}_{j} & \text{if } \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +``` + +### `Process_shut_down_rate` + +```yaml +Process_shut_down_rate: + description: >- + the shut-down ramp a process's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [process] + cases: + given: { when: Process_ramp_limit_shut_down, expression: Process_ramp_limit_shut_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} = \begin{cases} \mathrm{rd}^{z,\mathrm{dn}}_{j} & \text{if } \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +``` + +### `Process_p_nom_committed` + +```yaml +Process_p_nom_committed: + description: >- + the build a committed process's ramp rows are taken against — one module + where the build is extendable and modular, the given build otherwise + dims: [process] + cases: + modular_build: { when: Process_p_nom_extendable AND Process_p_nom_mod > 0, expression: Process_p_nom_mod } + otherwise: Process_p_nom +``` + +```math +\widehat{\mathrm{z}}^{\mathrm{nom}}_{j} = \begin{cases} \mathrm{z}^{\mathrm{mod}}_{j} & \text{if } \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \\ \mathrm{z}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +``` + ### `Process_ramp_up_allowance` ```yaml @@ -6016,14 +6417,14 @@ Process_ramp_up_allowance: committed: when: Process_committable expression: >- - Process_ramp_limit_up * Process_p_nom * Process_previous_status - + Process_ramp_limit_start_up * Process_p_nom + Process_ramp_up_rate * Process_p_nom_committed * Process_previous_status + + Process_start_up_rate * Process_p_nom_committed * (Process_status - Process_previous_status) - otherwise: Process_ramp_limit_up * Process_p_nom_effective + otherwise: Process_ramp_up_rate * Process_p_nom_effective ``` ```math -\Delta^{z,+}_{\xi,t,j} = \begin{cases} \mathrm{ru}^{z}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} + \mathrm{ru}^{z,\mathrm{up}}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \mathrm{ru}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\Delta^{z,+}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{ru}}^{z}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} + \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{ru}}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `Process_ramp_down_allowance` @@ -6039,14 +6440,14 @@ Process_ramp_down_allowance: committed: when: Process_committable expression: >- - Process_ramp_limit_down * Process_p_nom * Process_status - + Process_ramp_limit_shut_down * Process_p_nom + Process_ramp_down_rate * Process_p_nom_committed * Process_status + + Process_shut_down_rate * Process_p_nom_committed * (Process_previous_status - Process_status) - otherwise: Process_ramp_limit_down * Process_p_nom_effective + otherwise: Process_ramp_down_rate * Process_p_nom_effective ``` ```math -\Delta^{z,-}_{\xi,t,j} = \begin{cases} \mathrm{rd}^{z}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} + \mathrm{rd}^{z,\mathrm{dn}}_{j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \mathrm{rd}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\Delta^{z,-}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{rd}}^{z}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} + \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{rd}}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `StorageUnit_charge_carried_in` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 5f8490f3..289379ab 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -1193,6 +1193,46 @@ expressions: cases: extendable: { when: Generator_p_nom_extendable, expression: Generator_p_nom_ext } otherwise: Generator_p_nom + Generator_ramp_up_rate: + description: >- + the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } + otherwise: 1 + Generator_ramp_down_rate: + description: >- + the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } + otherwise: 1 + Generator_start_up_rate: + description: >- + the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_start_up, expression: Generator_ramp_limit_start_up } + otherwise: 1 + Generator_shut_down_rate: + description: >- + the shut-down ramp a unit's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_shut_down, expression: Generator_ramp_limit_shut_down } + otherwise: 1 + Generator_p_nom_committed: + description: >- + the build a committed unit's ramp rows are taken against — one module + where the build is extendable and modular, the given build otherwise + dims: [generator] + cases: + modular_build: { when: Generator_p_nom_extendable AND Generator_p_nom_mod > 0, expression: Generator_p_nom_mod } + otherwise: Generator_p_nom Generator_ramp_up_allowance: description: >- how far a generator may raise output between two snapshots — its ramp @@ -1203,10 +1243,10 @@ expressions: committed: when: Generator_committable expression: >- - Generator_ramp_limit_up * Generator_p_nom * Generator_previous_status - + Generator_ramp_limit_start_up * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom_committed * Generator_previous_status + + Generator_start_up_rate * Generator_p_nom_committed * (Generator_status - Generator_previous_status) - otherwise: Generator_ramp_limit_up * Generator_p_nom_effective + otherwise: Generator_ramp_up_rate * Generator_p_nom_effective Generator_ramp_down_allowance: description: >- how far a generator may lower output between two snapshots — its ramp @@ -1217,10 +1257,10 @@ expressions: committed: when: Generator_committable expression: >- - Generator_ramp_limit_down * Generator_p_nom * Generator_status - + Generator_ramp_limit_shut_down * Generator_p_nom + Generator_ramp_down_rate * Generator_p_nom_committed * Generator_status + + Generator_shut_down_rate * Generator_p_nom_committed * (Generator_previous_status - Generator_status) - otherwise: Generator_ramp_limit_down * Generator_p_nom_effective + otherwise: Generator_ramp_down_rate * Generator_p_nom_effective Link_p_nom_effective: description: the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise dims: [link] @@ -1244,6 +1284,46 @@ expressions: cases: opening: { when: "position(snapshot) == 0", expression: 0 } otherwise: shift(Link_p, along=snapshot, offset=1) + Link_ramp_up_rate: + description: >- + the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [link] + cases: + given: { when: Link_ramp_limit_up, expression: Link_ramp_limit_up } + otherwise: 1 + Link_ramp_down_rate: + description: >- + the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [link] + cases: + given: { when: Link_ramp_limit_down, expression: Link_ramp_limit_down } + otherwise: 1 + Link_start_up_rate: + description: >- + the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [link] + cases: + given: { when: Link_ramp_limit_start_up, expression: Link_ramp_limit_start_up } + otherwise: 1 + Link_shut_down_rate: + description: >- + the shut-down ramp a link's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [link] + cases: + given: { when: Link_ramp_limit_shut_down, expression: Link_ramp_limit_shut_down } + otherwise: 1 + Link_p_nom_committed: + description: >- + the build a committed link's ramp rows are taken against — one module + where the build is extendable and modular, the given build otherwise + dims: [link] + cases: + modular_build: { when: Link_p_nom_extendable AND Link_p_nom_mod > 0, expression: Link_p_nom_mod } + otherwise: Link_p_nom Link_ramp_up_allowance: description: >- how far a link may raise flow between two snapshots — its ramp @@ -1254,10 +1334,10 @@ expressions: committed: when: Link_committable expression: >- - Link_ramp_limit_up * Link_p_nom * Link_previous_status - + Link_ramp_limit_start_up * Link_p_nom + Link_ramp_up_rate * Link_p_nom_committed * Link_previous_status + + Link_start_up_rate * Link_p_nom_committed * (Link_status - Link_previous_status) - otherwise: Link_ramp_limit_up * Link_p_nom_effective + otherwise: Link_ramp_up_rate * Link_p_nom_effective Link_ramp_down_allowance: description: >- how far a link may lower flow between two snapshots — its ramp @@ -1268,10 +1348,10 @@ expressions: committed: when: Link_committable expression: >- - Link_ramp_limit_down * Link_p_nom * Link_status - + Link_ramp_limit_shut_down * Link_p_nom + Link_ramp_down_rate * Link_p_nom_committed * Link_status + + Link_shut_down_rate * Link_p_nom_committed * (Link_previous_status - Link_status) - otherwise: Link_ramp_limit_down * Link_p_nom_effective + otherwise: Link_ramp_down_rate * Link_p_nom_effective Process_p_nom_effective: description: the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise dims: [process] @@ -1295,6 +1375,46 @@ expressions: cases: opening: { when: "position(snapshot) == 0", expression: 0 } otherwise: shift(Process_p, along=snapshot, offset=1) + Process_ramp_up_rate: + description: >- + the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [process] + cases: + given: { when: Process_ramp_limit_up, expression: Process_ramp_limit_up } + otherwise: 1 + Process_ramp_down_rate: + description: >- + the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [process] + cases: + given: { when: Process_ramp_limit_down, expression: Process_ramp_limit_down } + otherwise: 1 + Process_start_up_rate: + description: >- + the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [process] + cases: + given: { when: Process_ramp_limit_start_up, expression: Process_ramp_limit_start_up } + otherwise: 1 + Process_shut_down_rate: + description: >- + the shut-down ramp a process's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [process] + cases: + given: { when: Process_ramp_limit_shut_down, expression: Process_ramp_limit_shut_down } + otherwise: 1 + Process_p_nom_committed: + description: >- + the build a committed process's ramp rows are taken against — one module + where the build is extendable and modular, the given build otherwise + dims: [process] + cases: + modular_build: { when: Process_p_nom_extendable AND Process_p_nom_mod > 0, expression: Process_p_nom_mod } + otherwise: Process_p_nom Process_ramp_up_allowance: description: >- how far a process may raise internal power between two snapshots — its ramp @@ -1305,10 +1425,10 @@ expressions: committed: when: Process_committable expression: >- - Process_ramp_limit_up * Process_p_nom * Process_previous_status - + Process_ramp_limit_start_up * Process_p_nom + Process_ramp_up_rate * Process_p_nom_committed * Process_previous_status + + Process_start_up_rate * Process_p_nom_committed * (Process_status - Process_previous_status) - otherwise: Process_ramp_limit_up * Process_p_nom_effective + otherwise: Process_ramp_up_rate * Process_p_nom_effective Process_ramp_down_allowance: description: >- how far a process may lower internal power between two snapshots — its ramp @@ -1319,10 +1439,10 @@ expressions: committed: when: Process_committable expression: >- - Process_ramp_limit_down * Process_p_nom * Process_status - + Process_ramp_limit_shut_down * Process_p_nom + Process_ramp_down_rate * Process_p_nom_committed * Process_status + + Process_shut_down_rate * Process_p_nom_committed * (Process_previous_status - Process_status) - otherwise: Process_ramp_limit_down * Process_p_nom_effective + otherwise: Process_ramp_down_rate * Process_p_nom_effective StorageUnit_charge_carried_in: description: >- the charge a unit opens a snapshot with — its last snapshot's less @@ -1657,11 +1777,12 @@ constraints: releases the row in the snapshot it turns on dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= - Generator_ramp_limit_up * Generator_p_nom_ext + Generator_ramp_up_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_previous_status Generator_p_ramp_limit_up_start_big_m: description: >- @@ -1670,11 +1791,12 @@ constraints: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_up + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_p - Generator_previous_p <= - Generator_ramp_limit_start_up * Generator_p_nom_ext + Generator_start_up_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_start_up Generator_p_ramp_limit_down_run_big_m: description: >- @@ -1683,11 +1805,12 @@ constraints: releases the row in the snapshot it turns off dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= - Generator_ramp_limit_down * Generator_p_nom_ext + Generator_ramp_down_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_status Generator_p_ramp_limit_down_shut_big_m: description: >- @@ -1696,11 +1819,12 @@ constraints: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, generator] where: >- - Generator_committable AND Generator_p_nom_extendable AND Generator_ramp_limit_down + Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) + AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active expression: >- Generator_previous_p - Generator_p <= - Generator_ramp_limit_shut_down * Generator_p_nom_ext + Generator_shut_down_rate * Generator_p_nom_ext + Generator_big_m - Generator_big_m * Generator_shut_down Generator_p_nom_modularity: description: "`Generator-p_nom_modularity` — the chosen build is a whole number of modules" @@ -1847,11 +1971,12 @@ constraints: releases the row in the snapshot it turns on dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_p - Link_previous_p <= - Link_ramp_limit_up * Link_p_nom_ext + Link_ramp_up_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_previous_status Link_p_ramp_limit_up_start_big_m: description: >- @@ -1860,11 +1985,12 @@ constraints: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_up + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_p - Link_previous_p <= - Link_ramp_limit_start_up * Link_p_nom_ext + Link_start_up_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_start_up Link_p_ramp_limit_down_run_big_m: description: >- @@ -1873,11 +1999,12 @@ constraints: releases the row in the snapshot it turns off dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_previous_p - Link_p <= - Link_ramp_limit_down * Link_p_nom_ext + Link_ramp_down_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_status Link_p_ramp_limit_down_shut_big_m: description: >- @@ -1886,11 +2013,12 @@ constraints: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, link] where: >- - Link_committable AND Link_p_nom_extendable AND Link_ramp_limit_down + Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) + AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active expression: >- Link_previous_p - Link_p <= - Link_ramp_limit_shut_down * Link_p_nom_ext + Link_shut_down_rate * Link_p_nom_ext + Link_big_m - Link_big_m * Link_shut_down Link_p_nom_modularity: description: "`Link-p_nom_modularity` — the chosen build is a whole number of modules" @@ -2037,11 +2165,12 @@ constraints: releases the row in the snapshot it turns on dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_p - Process_previous_p <= - Process_ramp_limit_up * Process_p_nom_ext + Process_ramp_up_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_previous_status Process_p_ramp_limit_up_start_big_m: description: >- @@ -2050,11 +2179,12 @@ constraints: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_up + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_p - Process_previous_p <= - Process_ramp_limit_start_up * Process_p_nom_ext + Process_start_up_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_start_up Process_p_ramp_limit_down_run_big_m: description: >- @@ -2063,11 +2193,12 @@ constraints: releases the row in the snapshot it turns off dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_previous_p - Process_p <= - Process_ramp_limit_down * Process_p_nom_ext + Process_ramp_down_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_status Process_p_ramp_limit_down_shut_big_m: description: >- @@ -2076,11 +2207,12 @@ constraints: the chosen build; the big M releases the row everywhere else dims: [scenario, snapshot, process] where: >- - Process_committable AND Process_p_nom_extendable AND Process_ramp_limit_down + Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) + AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active expression: >- Process_previous_p - Process_p <= - Process_ramp_limit_shut_down * Process_p_nom_ext + Process_shut_down_rate * Process_p_nom_ext + Process_big_m - Process_big_m * Process_shut_down Process_p_nom_modularity: description: "`Process-p_nom_modularity` — the chosen build is a whole number of modules" @@ -2316,8 +2448,8 @@ constraints: first snapshot — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- - Generator_ramp_limit_up - AND NOT (Generator_committable AND Generator_p_nom_extendable) + (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) + AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance Generator_p_ramp_limit_down: @@ -2329,8 +2461,8 @@ constraints: first snapshot — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- - Generator_ramp_limit_down - AND NOT (Generator_committable AND Generator_p_nom_extendable) + (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) + AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance Link_p_ramp_limit_up: @@ -2342,8 +2474,8 @@ constraints: first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- - Link_ramp_limit_up - AND NOT (Link_committable AND Link_p_nom_extendable) + (Link_ramp_limit_up OR Link_ramp_limit_start_up) + AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active expression: Link_p - Link_previous_p <= Link_ramp_up_allowance Link_p_ramp_limit_down: @@ -2355,8 +2487,8 @@ constraints: first snapshot — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- - Link_ramp_limit_down - AND NOT (Link_committable AND Link_p_nom_extendable) + (Link_ramp_limit_down OR Link_ramp_limit_shut_down) + AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active expression: Link_previous_p - Link_p <= Link_ramp_down_allowance Process_p_ramp_limit_up: @@ -2368,8 +2500,8 @@ constraints: first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- - Process_ramp_limit_up - AND NOT (Process_committable AND Process_p_nom_extendable) + (Process_ramp_limit_up OR Process_ramp_limit_start_up) + AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active expression: Process_p - Process_previous_p <= Process_ramp_up_allowance Process_p_ramp_limit_down: @@ -2381,8 +2513,8 @@ constraints: first snapshot — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- - Process_ramp_limit_down - AND NOT (Process_committable AND Process_p_nom_extendable) + (Process_ramp_limit_down OR Process_ramp_limit_shut_down) + AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active expression: Process_previous_p - Process_p <= Process_ramp_down_allowance StorageUnit_ext_p_dispatch_lower: diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index e464a0c0..c2d2677e 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -951,5 +951,128 @@ "Process-status-p-fixed-upper": 16, "Process-status-p_nom-variable-upper": 4 } + }, + "rung_27_modular_ramp": { + "columns": { + "Generator-n_mod": 1, + "Generator-p": 16, + "Generator-p_nom": 1, + "Generator-shut_down": 4, + "Generator-start_up": 4, + "Generator-status": 4, + "Link-n_mod": 1, + "Link-p": 8, + "Link-p_nom": 1, + "Link-shut_down": 4, + "Link-start_up": 4, + "Link-status": 4, + "Process-n_mod": 1, + "Process-p": 4, + "Process-p_nom": 1, + "Process-shut_down": 4, + "Process-start_up": 4, + "Process-status": 4 + }, + "global_constraints": {}, + "objective": 45469.49999999998, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-com-mod-p-lower": 4, + "Generator-com-mod-p-upper": 4, + "Generator-com-transition-shut-down": 4, + "Generator-com-transition-start-up": 4, + "Generator-ext-p_nom-lower": 1, + "Generator-ext-p_nom-upper": 1, + "Generator-fix-p-lower": 12, + "Generator-fix-p-upper": 12, + "Generator-p-ramp_limit_down": 4, + "Generator-p-ramp_limit_up": 4, + "Generator-p_nom_modularity": 1, + "Generator-shut_down-p_nom-variable-upper": 4, + "Generator-start_up-p_nom-variable-upper": 4, + "Generator-status-p_nom-variable-upper": 4, + "Link-com-mod-p-lower": 4, + "Link-com-mod-p-upper": 4, + "Link-com-transition-shut-down": 4, + "Link-com-transition-start-up": 4, + "Link-ext-p_nom-lower": 1, + "Link-ext-p_nom-upper": 1, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Link-p-ramp_limit_down": 4, + "Link-p-ramp_limit_up": 4, + "Link-p_nom_modularity": 1, + "Link-shut_down-p_nom-variable-upper": 4, + "Link-start_up-p_nom-variable-upper": 4, + "Link-status-p_nom-variable-upper": 4, + "Process-com-mod-p-lower": 4, + "Process-com-mod-p-upper": 4, + "Process-com-transition-shut-down": 4, + "Process-com-transition-start-up": 4, + "Process-ext-p_nom-lower": 1, + "Process-ext-p_nom-upper": 1, + "Process-p-ramp_limit_down": 4, + "Process-p-ramp_limit_up": 4, + "Process-p_nom_modularity": 1, + "Process-shut_down-p_nom-variable-upper": 4, + "Process-start_up-p_nom-variable-upper": 4, + "Process-status-p_nom-variable-upper": 4 + } + }, + "rung_28_start_up_ramp": { + "columns": { + "Generator-p": 16, + "Generator-shut_down": 4, + "Generator-start_up": 4, + "Generator-status": 4, + "Link-p": 8, + "Link-shut_down": 4, + "Link-start_up": 4, + "Link-status": 4, + "Process-p": 4, + "Process-shut_down": 4, + "Process-start_up": 4, + "Process-status": 4 + }, + "global_constraints": {}, + "objective": 83282.99999999983, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-com-p-lower": 4, + "Generator-com-p-upper": 4, + "Generator-com-transition-shut-down": 4, + "Generator-com-transition-start-up": 4, + "Generator-fix-p-lower": 12, + "Generator-fix-p-upper": 12, + "Generator-p-ramp_limit_down": 4, + "Generator-p-ramp_limit_up": 4, + "Generator-shut_down-p-fixed-upper": 4, + "Generator-start_up-p-fixed-upper": 4, + "Generator-status-p-fixed-upper": 4, + "Link-com-p-lower": 4, + "Link-com-p-upper": 4, + "Link-com-transition-shut-down": 4, + "Link-com-transition-start-up": 4, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Link-p-ramp_limit_down": 4, + "Link-p-ramp_limit_up": 4, + "Link-shut_down-p-fixed-upper": 4, + "Link-start_up-p-fixed-upper": 4, + "Link-status-p-fixed-upper": 4, + "Process-com-p-lower": 4, + "Process-com-p-upper": 4, + "Process-com-transition-shut-down": 4, + "Process-com-transition-start-up": 4, + "Process-p-ramp_limit_down": 4, + "Process-p-ramp_limit_up": 4, + "Process-shut_down-p-fixed-upper": 4, + "Process-start_up-p-fixed-upper": 4, + "Process-status-p-fixed-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_27_modular_ramp.py b/examples/references/pypsa/rung_27_modular_ramp.py new file mode 100644 index 00000000..54bdee66 --- /dev/null +++ b/examples/references/pypsa/rung_27_modular_ramp.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 27: modular ramps — a committable extendable modular unit ramps against one module through the ordinary ramp rows, not the big-M ones.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a peak bus served by a committable modular generator, link and process, each ramp-limited, with a dear backup.""" + n = spine.build() + n.add('Bus', 'peak') + common = { + 'committable': True, + 'p_nom_extendable': True, + 'p_nom_mod': 20, + 'p_nom_max': 60, + 'capital_cost': 2, + 'p_min_pu': 0.2, + 'up_time_before': 0, + 'ramp_limit_up': 0.5, + 'ramp_limit_down': 0.5, + 'ramp_limit_start_up': 0.6, + 'ramp_limit_shut_down': 0.6, + } + n.add('Generator', 'mod_gen', bus='peak', marginal_cost=3, **common) + n.add('Link', 'mod_link', bus0='north', bus1='peak', marginal_cost=4, **common) + n.add('Process', 'mod_proc', bus0='south', bus1='peak', rate0=-1.25, marginal_cost=5, **common) + n.add('Generator', 'peak_backup', bus='peak', p_nom=200, marginal_cost=500) + n.add('Load', 'peak_load', bus='peak', p_set=[10, 90, 150, 20]) + return n diff --git a/examples/references/pypsa/rung_28_start_up_ramp.py b/examples/references/pypsa/rung_28_start_up_ramp.py new file mode 100644 index 00000000..cfabe91f --- /dev/null +++ b/examples/references/pypsa/rung_28_start_up_ramp.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 28: a start-up ramp alone — a committable unit with only a start-up and a shut-down ramp still gets ramp rows, at the full build between them.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a pulse bus served by committable generator, link and process that carry only start-up and shut-down ramps, with a dear backup.""" + n = spine.build() + n.add('Bus', 'pulse') + common = { + 'committable': True, + 'p_nom': 40, + 'p_min_pu': 0.1, + 'up_time_before': 0, + 'ramp_limit_start_up': 0.4, + 'ramp_limit_shut_down': 0.5, + } + n.add('Generator', 'pulse_gen', bus='pulse', marginal_cost=3, **common) + n.add('Link', 'pulse_link', bus0='north', bus1='pulse', marginal_cost=4, **common) + n.add('Process', 'pulse_proc', bus0='south', bus1='pulse', rate0=-1.25, marginal_cost=5, **common) + n.add('Generator', 'pulse_backup', bus='pulse', p_nom=200, marginal_cost=500) + n.add('Load', 'pulse_load', bus='pulse', p_set=[0, 60, 110, 0]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 5705fda4..a8e2704e 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -46,6 +46,11 @@ names: Generator_previous_p: '\overleftarrow{p}' Generator_ramp_up_allowance: '\Delta^{+}' Generator_ramp_down_allowance: '\Delta^{-}' + Generator_ramp_up_rate: '\widetilde{\mathrm{ru}}' + Generator_ramp_down_rate: '\widetilde{\mathrm{rd}}' + Generator_start_up_rate: '\widetilde{\mathrm{ru}}^{\mathrm{up}}' + Generator_shut_down_rate: '\widetilde{\mathrm{rd}}^{\mathrm{dn}}' + Generator_p_nom_committed: '\widehat{\mathrm{p}}^{\mathrm{nom}}' Generator_must_stay_up: '\mathrm{hold}' Generator_must_stay_down: '\mathrm{rest}' Generator_start_up_cost: '\mathrm{c}^{\mathrm{up}}' @@ -81,6 +86,11 @@ names: Link_previous_p: '\overleftarrow{f}' Link_ramp_up_allowance: '\Delta^{f,+}' Link_ramp_down_allowance: '\Delta^{f,-}' + Link_ramp_up_rate: '\widetilde{\mathrm{ru}}^{f}' + Link_ramp_down_rate: '\widetilde{\mathrm{rd}}^{f}' + Link_start_up_rate: '\widetilde{\mathrm{ru}}^{f,\mathrm{up}}' + Link_shut_down_rate: '\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}' + Link_p_nom_committed: '\widehat{\mathrm{f}}^{\mathrm{nom}}' Link_must_stay_up: '\mathrm{hold}^{f}' Link_must_stay_down: '\mathrm{rest}^{f}' Link_start_up_cost: '\mathrm{c}^{f,\mathrm{up}}' @@ -143,6 +153,11 @@ names: Process_previous_p: '\overleftarrow{z}' Process_ramp_up_allowance: '\Delta^{z,+}' Process_ramp_down_allowance: '\Delta^{z,-}' + Process_ramp_up_rate: '\widetilde{\mathrm{ru}}^{z}' + Process_ramp_down_rate: '\widetilde{\mathrm{rd}}^{z}' + Process_start_up_rate: '\widetilde{\mathrm{ru}}^{z,\mathrm{up}}' + Process_shut_down_rate: '\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}' + Process_p_nom_committed: '\widehat{\mathrm{z}}^{\mathrm{nom}}' Process_must_stay_up: '\mathrm{hold}^{z}' Process_must_stay_down: '\mathrm{rest}^{z}' Process_start_up_cost: '\mathrm{c}^{z,\mathrm{up}}' From 805d4805e0f24d5ff06a05c917e8b63123475565 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 13:02:31 +0200 Subject: [PATCH 14/46] docs(pypsa): storage cycles or reopens per investment period, and a ramp restarts at a period start --- docs/examples/pypsa.md | 233 ++++++++++++++---- examples/pypsa.yaml | 128 +++++++--- examples/references/pypsa/references.json | 31 +++ .../pypsa/rung_29_storage_per_period.py | 45 ++++ examples/symbols/pypsa.yaml | 4 + 5 files changed, 358 insertions(+), 83 deletions(-) create mode 100644 examples/references/pypsa/rung_29_storage_per_period.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index f22c2b8f..d6c8d4e8 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1357,7 +1357,7 @@ own dimensions. | [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | | [`Carrier-growth_limit`](#carrier-growth_limit) | done | every extendable component of the carrier, counted in the first period a build stands in; `edge=0` at the first period | | [objective](#objective) | done | period weight on operation; capacity once per period it stands in | -| `StorageUnit-energy_balance` per period, ramps at period starts | out | `shift(…, by=snapshot_period, within=period)` has them; a later rung | +| [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12747.19109626398`, 80 rows. @@ -2397,6 +2397,87 @@ def build(): +### Rung 29 — storage per investment period + +`n.optimize(multi_investment_periods=True)` with storage that treats each +investment period as a horizon of its own. A storage unit with +`cyclic_state_of_charge_per_period` and a store with `e_cyclic_per_period` +close each period on itself: the first snapshot of a period carries in the +level of that period's last snapshot. A storage unit with +`state_of_charge_initial_per_period` and a store with `e_initial_per_period` +open each period on their initial level. The per-period cyclic flag overrides +the global one and the per-period initial flag. PyPSA reads the four flags only +under `multi_investment_periods`, so data prep feeds false on a plain run. + +PyPSA also builds no ramp row at the first snapshot of a later period, with or +without these flags. The ramp-limited coal unit in this rung raises its output +from 59.2 to 90 across the period boundary, above its limit of 10 per +snapshot, while its ramp rows bind inside each period. + +| PyPSA | status | note | +| --- | --- | --- | +| [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance), per period | done | two more cases in the charge carried in: a `shift(…, edge='wrap', by=snapshot_period, within=period)` and the initial level at `position(snapshot, by=snapshot_period, within=period) == 0` | +| [`{c}-p-ramp_limit_*`, `-bigM`, at a period start](#generator-p-ramp_limit_up) | done | the `where:` drops every period start but the horizon's first | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `7438.461538461539`, 212 rows. + +
+The network, as PyPSA code + +`rung_29_storage_per_period.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 29: storage per investment period — a storage unit and a store that cycle within each period, two that reopen on their initial level, and a ramp that restarts at a period start.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: eight snapshots over two periods, four storages that each close or reopen per period, a ramp-limited coal unit.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 2.0, 1.5, 2.5, 2.0] + n.snapshot_weightings['stores'] = [0.5, 2.0, 1.5, 2.5, 0.5, 2.0, 1.5, 2.5] + n.add('Bus', 'hub') + n.add('Generator', 'coal29', bus='hub', p_nom=100, marginal_cost=10, ramp_limit_up=0.1, ramp_limit_down=0.1) + n.add('Generator', 'peak29', bus='hub', p_nom=200, marginal_cost=[80, 20, 90, 30, 80, 20, 90, 30]) + n.add('StorageUnit', 'su_cycle', bus='hub', p_nom=15, max_hours=4, cyclic_state_of_charge_per_period=True) + n.add( + 'StorageUnit', + 'su_reset', + bus='hub', + p_nom=15, + max_hours=4, + state_of_charge_initial=20, + state_of_charge_initial_per_period=True, + ) + n.add('Store', 'e_cycle', bus='hub', e_nom=30, e_cyclic_per_period=True) + n.add('Store', 'e_reset', bus='hub', e_nom=30, e_initial=10, e_initial_per_period=True) + n.add('Load', 'hub_load', bus='hub', p_set=[40, 60, 70, 40, 90, 110, 120, 90]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2590,6 +2671,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{inflow}`$ | `StorageUnit_inflow` over $`\mathcal{T} \times \mathcal{S}`$ — energy arriving per hour, a river into a reservoir | | $`\mathrm{soc}^{0}`$ | `StorageUnit_state_of_charge_initial` over $`\mathcal{S}`$ — charge held before the first snapshot | | $`\mathrm{cyc}`$ | `StorageUnit_cyclic_state_of_charge` over $`\mathcal{S}`$ — whether the horizon closes on itself instead of opening on the initial charge | +| $`\mathrm{cyc}^{y}`$ | `StorageUnit_cyclic_state_of_charge_per_period` over $`\mathcal{S}`$ — whether each investment period closes on itself instead of carrying its charge on to the next; it overrides `cyclic_state_of_charge` and `state_of_charge_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\mathrm{reset}`$ | `StorageUnit_state_of_charge_initial_per_period` over $`\mathcal{S}`$ — whether each investment period opens on the initial charge instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{c}^{h}`$ | `StorageUnit_marginal_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of dispatch | | $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | | $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | @@ -2602,6 +2685,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\rho^{e}`$ | `Store_retention` over $`\mathcal{T} \times \mathcal{V}`$ — share of energy kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep | | $`\mathrm{e}^{0}`$ | `Store_e_initial` over $`\mathcal{V}`$ — energy held before the first snapshot | | $`\mathrm{cyc}^{e}`$ | `Store_e_cyclic` over $`\mathcal{V}`$ — whether the horizon closes on itself instead of opening on the initial energy | +| $`\mathrm{cyc}^{e,y}`$ | `Store_e_cyclic_per_period` over $`\mathcal{V}`$ — whether each investment period closes on itself instead of carrying its energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\mathrm{reset}^{e}`$ | `Store_e_initial_per_period` over $`\mathcal{V}`$ — whether each investment period opens on the initial energy instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | | $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | | $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | @@ -2731,8 +2816,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\widehat{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_committed` over $`\mathcal{J}`$ — the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{z,+}`$ | `Process_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may raise internal power between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{z,-}`$ | `Process_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may lower internal power between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | -| $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | -| $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise | +| $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise. Per period, the same holds with each investment period as the horizon | +| $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise. Per period, the same holds with each investment period as the horizon | | $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | | $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed transfer wraps from the horizon's end, and where it is not the energy still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | | $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{I}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | @@ -2749,8 +2834,12 @@ $`t \ominus k`$ denotes cyclic translation: index $`t-k`$ taken modulo the size $`t \boxminus_{v} k`$ denotes translation with $`v`$ standing where index $`t-k`$ leaves the dimension (`shift(edge=v)`), so the row at that boundary is built and carries $`v`$ rather than being dropped. +$`t \ominus^{\mathrm{relation}(t)} k`$ denotes a translation counted inside the group a relation puts $`t`$ in (`shift(by=relation)`), so a term never crosses out of its own group. The two modifiers take different slots — the group above, the fill below — so $`t \boxminus_{v}^{\mathrm{relation}(t)} k`$ is both at once. + $`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, not the order labels sort in — counted from $`0`$. The index itself stays the coordinate, so $`t`$ compares against labels and $`\mathrm{pos}(t)`$ against positions. +$`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ counts within the group a relation puts $`t`$ in: the subscript names the map, $`\mathcal{T}_{\mathrm{relation}(t)}`$ is the group it lands in, and that group has a first position of its own. + ### Objective ```yaml @@ -3389,7 +3478,8 @@ Generator_p_ramp_limit_up_run_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_ramp_up_rate * Generator_p_nom_ext @@ -3397,7 +3487,7 @@ Generator_p_ramp_limit_up_run_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-start-bigM` @@ -3414,7 +3504,8 @@ Generator_p_ramp_limit_up_start_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_start_up_rate * Generator_p_nom_ext @@ -3422,7 +3513,7 @@ Generator_p_ramp_limit_up_start_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-run-bigM` @@ -3439,7 +3530,8 @@ Generator_p_ramp_limit_down_run_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_ramp_down_rate * Generator_p_nom_ext @@ -3447,7 +3539,7 @@ Generator_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-shut-bigM` @@ -3464,7 +3556,8 @@ Generator_p_ramp_limit_down_shut_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_shut_down_rate * Generator_p_nom_ext @@ -3472,7 +3565,7 @@ Generator_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p_nom_modularity` @@ -3858,7 +3951,8 @@ Link_p_ramp_limit_up_run_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_p - Link_previous_p <= Link_ramp_up_rate * Link_p_nom_ext @@ -3866,7 +3960,7 @@ Link_p_ramp_limit_up_run_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_up-start-bigM` @@ -3883,7 +3977,8 @@ Link_p_ramp_limit_up_start_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_p - Link_previous_p <= Link_start_up_rate * Link_p_nom_ext @@ -3891,7 +3986,7 @@ Link_p_ramp_limit_up_start_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-run-bigM` @@ -3908,7 +4003,8 @@ Link_p_ramp_limit_down_run_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_previous_p - Link_p <= Link_ramp_down_rate * Link_p_nom_ext @@ -3916,7 +4012,7 @@ Link_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-shut-bigM` @@ -3933,7 +4029,8 @@ Link_p_ramp_limit_down_shut_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_previous_p - Link_p <= Link_shut_down_rate * Link_p_nom_ext @@ -3941,7 +4038,7 @@ Link_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p_nom_modularity` @@ -4327,7 +4424,8 @@ Process_p_ramp_limit_up_run_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_p - Process_previous_p <= Process_ramp_up_rate * Process_p_nom_ext @@ -4335,7 +4433,7 @@ Process_p_ramp_limit_up_run_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_up-start-bigM` @@ -4352,7 +4450,8 @@ Process_p_ramp_limit_up_start_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_p - Process_previous_p <= Process_start_up_rate * Process_p_nom_ext @@ -4360,7 +4459,7 @@ Process_p_ramp_limit_up_start_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-run-bigM` @@ -4377,7 +4476,8 @@ Process_p_ramp_limit_down_run_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_previous_p - Process_p <= Process_ramp_down_rate * Process_p_nom_ext @@ -4385,7 +4485,7 @@ Process_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-shut-bigM` @@ -4402,7 +4502,8 @@ Process_p_ramp_limit_down_shut_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_previous_p - Process_p <= Process_shut_down_rate * Process_p_nom_ext @@ -4410,7 +4511,7 @@ Process_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p_nom_modularity` @@ -5045,17 +5146,18 @@ Generator_p_ramp_limit_up: its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A unit that came into the horizon running brought an unknown output, so it carries no row at the - first snapshot — nor does any unit a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down` @@ -5069,17 +5171,18 @@ Generator_p_ramp_limit_down: its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A unit that came into the horizon running brought an unknown output, so it carries no row at the - first snapshot — nor does any unit a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Link-p-ramp_limit_up` @@ -5093,17 +5196,18 @@ Link_p_ramp_limit_up: its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A link that came into the horizon running brought an unknown flow, so it carries no row at the - first snapshot — nor does any link a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND Link_active expression: Link_p - Link_previous_p <= Link_ramp_up_allowance ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down` @@ -5117,17 +5221,18 @@ Link_p_ramp_limit_down: its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A link that came into the horizon running brought an unknown flow, so it carries no row at the - first snapshot — nor does any link a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND Link_active expression: Link_previous_p - Link_p <= Link_ramp_down_allowance ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-p-ramp_limit_up` @@ -5141,17 +5246,18 @@ Process_p_ramp_limit_up: its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A process that came into the horizon running brought an unknown internal power, so it carries no row at the - first snapshot — nor does any process a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND Process_active expression: Process_p - Process_previous_p <= Process_ramp_up_allowance ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down` @@ -5165,17 +5271,18 @@ Process_p_ramp_limit_down: its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A process that came into the horizon running brought an unknown internal power, so it carries no row at the - first snapshot — nor does any process a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND Process_active expression: Process_previous_p - Process_p <= Process_ramp_down_allowance ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `StorageUnit-ext-p_dispatch-lower` @@ -6458,20 +6565,33 @@ StorageUnit_charge_carried_in: the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise + snapshot's less standing loss otherwise. Per period, the same holds with + each investment period as the horizon dims: [scenario, snapshot, storage_unit] cases: cyclic: - when: StorageUnit_cyclic_state_of_charge + when: StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period AND NOT StorageUnit_state_of_charge_initial_per_period expression: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap') opening: - when: not StorageUnit_cyclic_state_of_charge AND position(snapshot) == 0 + when: >- + NOT StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period + AND NOT StorageUnit_state_of_charge_initial_per_period AND position(snapshot) == 0 + expression: StorageUnit_state_of_charge_initial + period_cyclic: + when: StorageUnit_cyclic_state_of_charge_per_period + expression: >- + StorageUnit_retention + * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', by=snapshot_period, within=period) + period_opening: + when: >- + StorageUnit_state_of_charge_initial_per_period AND NOT StorageUnit_cyclic_state_of_charge_per_period + AND position(snapshot, by=snapshot_period, within=period) == 0 expression: StorageUnit_state_of_charge_initial otherwise: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1) ``` ```math -\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus 1,s} & \text{if } \mathrm{cyc}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \neg \mathrm{cyc}_{s} \wedge \mathrm{pos}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} +\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus 1,s} & \text{if } \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \neg \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \wedge \mathrm{pos}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \mathrm{reset}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} ``` ### `Store_energy_carried_in` @@ -6482,20 +6602,27 @@ Store_energy_carried_in: the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise + snapshot's less standing loss otherwise. Per period, the same holds with + each investment period as the horizon dims: [scenario, snapshot, store] cases: cyclic: - when: Store_e_cyclic + when: Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap') opening: - when: not Store_e_cyclic AND position(snapshot) == 0 + when: NOT Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period AND position(snapshot) == 0 + expression: Store_e_initial + period_cyclic: + when: Store_e_cyclic_per_period + expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', by=snapshot_period, within=period) + period_opening: + when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, by=snapshot_period, within=period) == 0 expression: Store_e_initial otherwise: Store_retention * shift(Store_e, along=snapshot, offset=1) ``` ```math -\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{t,v} \cdot e_{\xi,t \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \neg \mathrm{cyc}^{e}_{v} \wedge \mathrm{pos}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} +\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{t,v} \cdot e_{\xi,t \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \neg \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \mathrm{pos}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \mathrm{reset}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} ``` ### `Link_output_arrival` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 289379ab..4f7efc54 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -580,6 +580,21 @@ parameters: description: whether the horizon closes on itself instead of opening on the initial charge dims: [storage_unit] dtype: bool + StorageUnit_cyclic_state_of_charge_per_period: + description: >- + whether each investment period closes on itself instead of carrying its + charge on to the next; it overrides `cyclic_state_of_charge` and + `state_of_charge_initial_per_period`. PyPSA reads it only under + `multi_investment_periods`, so data prep feeds false otherwise + dims: [storage_unit] + dtype: bool + StorageUnit_state_of_charge_initial_per_period: + description: >- + whether each investment period opens on the initial charge instead of + carrying the previous period's; PyPSA reads it only under + `multi_investment_periods`, so data prep feeds false otherwise + dims: [storage_unit] + dtype: bool StorageUnit_marginal_cost: description: cost of one unit of dispatch dims: [snapshot, storage_unit] @@ -618,6 +633,21 @@ parameters: description: whether the horizon closes on itself instead of opening on the initial energy dims: [store] dtype: bool + Store_e_cyclic_per_period: + description: >- + whether each investment period closes on itself instead of carrying its + energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`. + PyPSA reads it only under `multi_investment_periods`, so data prep feeds + false otherwise + dims: [store] + dtype: bool + Store_e_initial_per_period: + description: >- + whether each investment period opens on the initial energy instead of + carrying the previous period's; PyPSA reads it only under + `multi_investment_periods`, so data prep feeds false otherwise + dims: [store] + dtype: bool Store_marginal_cost: description: cost of one unit of power delivered dims: [snapshot, store] @@ -1448,14 +1478,27 @@ expressions: the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise + snapshot's less standing loss otherwise. Per period, the same holds with + each investment period as the horizon dims: [scenario, snapshot, storage_unit] cases: cyclic: - when: StorageUnit_cyclic_state_of_charge + when: StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period AND NOT StorageUnit_state_of_charge_initial_per_period expression: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap') opening: - when: not StorageUnit_cyclic_state_of_charge AND position(snapshot) == 0 + when: >- + NOT StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period + AND NOT StorageUnit_state_of_charge_initial_per_period AND position(snapshot) == 0 + expression: StorageUnit_state_of_charge_initial + period_cyclic: + when: StorageUnit_cyclic_state_of_charge_per_period + expression: >- + StorageUnit_retention + * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', by=snapshot_period, within=period) + period_opening: + when: >- + StorageUnit_state_of_charge_initial_per_period AND NOT StorageUnit_cyclic_state_of_charge_per_period + AND position(snapshot, by=snapshot_period, within=period) == 0 expression: StorageUnit_state_of_charge_initial otherwise: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1) Store_energy_carried_in: @@ -1463,14 +1506,21 @@ expressions: the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise + snapshot's less standing loss otherwise. Per period, the same holds with + each investment period as the horizon dims: [scenario, snapshot, store] cases: cyclic: - when: Store_e_cyclic + when: Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap') opening: - when: not Store_e_cyclic AND position(snapshot) == 0 + when: NOT Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period AND position(snapshot) == 0 + expression: Store_e_initial + period_cyclic: + when: Store_e_cyclic_per_period + expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', by=snapshot_period, within=period) + period_opening: + when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, by=snapshot_period, within=period) == 0 expression: Store_e_initial otherwise: Store_retention * shift(Store_e, along=snapshot, offset=1) Link_output_arrival: @@ -1779,7 +1829,8 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_ramp_up_rate * Generator_p_nom_ext @@ -1793,7 +1844,8 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_p - Generator_previous_p <= Generator_start_up_rate * Generator_p_nom_ext @@ -1807,7 +1859,8 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_ramp_down_rate * Generator_p_nom_ext @@ -1821,7 +1874,8 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND Generator_active expression: >- Generator_previous_p - Generator_p <= Generator_shut_down_rate * Generator_p_nom_ext @@ -1973,7 +2027,8 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_p - Link_previous_p <= Link_ramp_up_rate * Link_p_nom_ext @@ -1987,7 +2042,8 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_p - Link_previous_p <= Link_start_up_rate * Link_p_nom_ext @@ -2001,7 +2057,8 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_previous_p - Link_p <= Link_ramp_down_rate * Link_p_nom_ext @@ -2015,7 +2072,8 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Link_status_initial == 0) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND Link_active expression: >- Link_previous_p - Link_p <= Link_shut_down_rate * Link_p_nom_ext @@ -2167,7 +2225,8 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_p - Process_previous_p <= Process_ramp_up_rate * Process_p_nom_ext @@ -2181,7 +2240,8 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_p - Process_previous_p <= Process_start_up_rate * Process_p_nom_ext @@ -2195,7 +2255,8 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_previous_p - Process_p <= Process_ramp_down_rate * Process_p_nom_ext @@ -2209,7 +2270,8 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Process_status_initial == 0) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND Process_active expression: >- Process_previous_p - Process_p <= Process_shut_down_rate * Process_p_nom_ext @@ -2445,12 +2507,13 @@ constraints: its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A unit that came into the horizon running brought an unknown output, so it carries no row at the - first snapshot — nor does any unit a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance Generator_p_ramp_limit_down: description: >- @@ -2458,12 +2521,13 @@ constraints: its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A unit that came into the horizon running brought an unknown output, so it carries no row at the - first snapshot — nor does any unit a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Generator_committable AND Generator_status_initial == 0)) AND Generator_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance Link_p_ramp_limit_up: description: >- @@ -2471,12 +2535,13 @@ constraints: its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A link that came into the horizon running brought an unknown flow, so it carries no row at the - first snapshot — nor does any link a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND Link_active expression: Link_p - Link_previous_p <= Link_ramp_up_allowance Link_p_ramp_limit_down: description: >- @@ -2484,12 +2549,13 @@ constraints: its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A link that came into the horizon running brought an unknown flow, so it carries no row at the - first snapshot — nor does any link a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Link_committable AND Link_status_initial == 0)) AND Link_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND Link_active expression: Link_previous_p - Link_p <= Link_ramp_down_allowance Process_p_ramp_limit_up: description: >- @@ -2497,12 +2563,13 @@ constraints: its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A process that came into the horizon running brought an unknown internal power, so it carries no row at the - first snapshot — nor does any process a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND Process_active expression: Process_p - Process_previous_p <= Process_ramp_up_allowance Process_p_ramp_limit_down: description: >- @@ -2510,12 +2577,13 @@ constraints: its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A process that came into the horizon running brought an unknown internal power, so it carries no row at the - first snapshot — nor does any process a big M releases instead + first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot) > 0 OR (Process_committable AND Process_status_initial == 0)) AND Process_active + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND Process_active expression: Process_previous_p - Process_p <= Process_ramp_down_allowance StorageUnit_ext_p_dispatch_lower: description: "`StorageUnit-ext-p_dispatch-lower` — dispatch is non-negative" diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index c2d2677e..62204118 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1074,5 +1074,36 @@ "Process-start_up-p-fixed-upper": 4, "Process-status-p-fixed-upper": 4 } + }, + "rung_29_storage_per_period": { + "columns": { + "Generator-p": 16, + "StorageUnit-p_dispatch": 16, + "StorageUnit-p_store": 16, + "StorageUnit-state_of_charge": 16, + "Store-e": 16, + "Store-p": 16 + }, + "global_constraints": {}, + "objective": 7438.461538461539, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Generator-p-ramp_limit_down": 6, + "Generator-p-ramp_limit_up": 6, + "StorageUnit-energy_balance": 16, + "StorageUnit-fix-p_dispatch-lower": 16, + "StorageUnit-fix-p_dispatch-upper": 16, + "StorageUnit-fix-p_store-lower": 16, + "StorageUnit-fix-p_store-upper": 16, + "StorageUnit-fix-state_of_charge-lower": 16, + "StorageUnit-fix-state_of_charge-upper": 16, + "Store-energy_balance": 16, + "Store-fix-e-lower": 16, + "Store-fix-e-upper": 16 + } } } diff --git a/examples/references/pypsa/rung_29_storage_per_period.py b/examples/references/pypsa/rung_29_storage_per_period.py new file mode 100644 index 00000000..7177e8df --- /dev/null +++ b/examples/references/pypsa/rung_29_storage_per_period.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 29: storage per investment period — a storage unit and a store that cycle within each period, two that reopen on their initial level, and a ramp that restarts at a period start.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: eight snapshots over two periods, four storages that each close or reopen per period, a ramp-limited coal unit.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 2.0, 1.5, 2.5, 2.0] + n.snapshot_weightings['stores'] = [0.5, 2.0, 1.5, 2.5, 0.5, 2.0, 1.5, 2.5] + n.add('Bus', 'hub') + n.add('Generator', 'coal29', bus='hub', p_nom=100, marginal_cost=10, ramp_limit_up=0.1, ramp_limit_down=0.1) + n.add('Generator', 'peak29', bus='hub', p_nom=200, marginal_cost=[80, 20, 90, 30, 80, 20, 90, 30]) + n.add('StorageUnit', 'su_cycle', bus='hub', p_nom=15, max_hours=4, cyclic_state_of_charge_per_period=True) + n.add( + 'StorageUnit', + 'su_reset', + bus='hub', + p_nom=15, + max_hours=4, + state_of_charge_initial=20, + state_of_charge_initial_per_period=True, + ) + n.add('Store', 'e_cycle', bus='hub', e_nom=30, e_cyclic_per_period=True) + n.add('Store', 'e_reset', bus='hub', e_nom=30, e_initial=10, e_initial_per_period=True) + n.add('Load', 'hub_load', bus='hub', p_set=[40, 60, 70, 40, 90, 110, 120, 90]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index a8e2704e..6eafe555 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -246,6 +246,8 @@ names: StorageUnit_state_of_charge_initial: '\mathrm{soc}^{0}' StorageUnit_charge_carried_in: '\overleftarrow{\mathit{soc}}' StorageUnit_cyclic_state_of_charge: '\mathrm{cyc}' + StorageUnit_cyclic_state_of_charge_per_period: '\mathrm{cyc}^{y}' + StorageUnit_state_of_charge_initial_per_period: '\mathrm{reset}' StorageUnit_marginal_cost: '\mathrm{c}^{h}' StorageUnit_marginal_cost_storage: '\mathrm{c}^{\mathrm{soc}}' StorageUnit_spill_cost: '\mathrm{c}^{\mathrm{spill}}' @@ -261,6 +263,8 @@ names: Store_e_initial: '\mathrm{e}^{0}' Store_energy_carried_in: '\overleftarrow{e}' Store_e_cyclic: '\mathrm{cyc}^{e}' + Store_e_cyclic_per_period: '\mathrm{cyc}^{e,y}' + Store_e_initial_per_period: '\mathrm{reset}^{e}' Store_marginal_cost: '\mathrm{c}^{q}' Store_marginal_cost_storage: '\mathrm{c}^{e}' Store_e_set: '\mathrm{e}^{\mathrm{set}}' From 2a7d5e95b32cdebd4377b19ed19b46be3376e349 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 13:17:49 +0200 Subject: [PATCH 15/46] docs(pypsa): a security-constrained run limits every branch flow after any one listed outage --- docs/examples/pypsa.md | 315 +++++++++++++++++- examples/pypsa.yaml | 141 +++++++- examples/references/pypsa/reference.py | 11 +- examples/references/pypsa/references.json | 50 +++ .../pypsa/rung_30_security_constrained.py | 30 ++ examples/symbols/pypsa.yaml | 6 + 6 files changed, 544 insertions(+), 9 deletions(-) create mode 100644 examples/references/pypsa/rung_30_security_constrained.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index d6c8d4e8..5788a998 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -24,8 +24,9 @@ A row is **done** once the file states it as the one block PyPSA builds. **split** means the same feasible region and optimum under a different statement, such as several `where:` blocks. **open** means not stated yet. **out** means never stated, deliberately: emitted only under the keyword, -scope or version the note names. A name carrying `{k}` or `{s}` stands for the -family PyPSA numbers per segment or scenario. +scope or version the note names. A name carrying `{k}`, `{s}`, `{c}` or `{n}` stands +for the family PyPSA numbers per segment, scenario, outaged component or +sub-network. Each rung's banner states what PyPSA solved its reference network to. @@ -2478,6 +2479,79 @@ def build(): +### Rung 30 — security-constrained + +`n.optimize.optimize_security_constrained(branch_outages=...)`: after any one +outage of a listed passive branch, every branch of the same sub-network carries +its flow within its rating. PyPSA computes the sub-network's branch outage +distribution factors (BODF) and copies each flow limit row with the outaged +branch's flow, times its factor, added to the left-hand side +(`abstract.py:443-489`). The copy keeps the row's sense, right-hand side and +loss term, and its extendable rating. An outage is a line or a transformer, and +a plain list names lines. The outaged branch is monitored too, at the factor +`-1`. The file states the copies over an `outage` axis, with the factors as +data prep, `Line_BODF` and `Transformer_BODF`. A plain run supplies no outage, +so no copy is built and the model collapses to the standard one. + +The rung outages two lines and a transformer of a meshed triangle and leaves +the third line monitored only. A plain `n.optimize()` solves the same network +at objective `17380.0`; the outages raise it to `22113.33` (#620). The cheap +unit at `a` falls to 32 in every snapshot, the unit at `b` covers the rest, and +the extendable line `ca` builds 20.7 instead of 2. Six of the 120 copied rows +bind, in `Transformer-fix-s-lower` against a line outage and in +`Line-ext-s-lower` against a transformer outage. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Line-fix-s-*-security-for-{c}-outage-in-sub-network-{n}`](#line-fix-s-lower-security-for-c-outage-in-sub-network-n), [`Line-ext-s-*-security-…`](#line-ext-s-lower-security-for-c-outage-in-sub-network-n) | split | PyPSA names a row per outaged component and sub-network; one block over the `outage` axis | +| [`Transformer-fix-s-*-security-…`](#transformer-fix-s-lower-security-for-c-outage-in-sub-network-n), [`Transformer-ext-s-*-security-…`](#transformer-ext-s-lower-security-for-c-outage-in-sub-network-n) | split | the same for a transformer | +| a branch not active in a period | done | PyPSA keeps the copy with that branch's flow dropped, so the file reads its flow as zero there; a copy left with no variable is not built here, where linopy counts it; no rung records it | +| a security-constrained run over scenarios | out | PyPSA `1.3.0` raises, see [Refusals](#refusals) | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `22113.333333333332`, 240 rows. + +
+The network, as PyPSA code + +`rung_30_security_constrained.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 30: security-constrained — a meshed triangle of lines and two transformers, one extendable, that must carry their flow within their rating after any one of three outages.""" + +from __future__ import annotations + +import pandas as pd +import spine + +BRANCH_OUTAGES = pd.MultiIndex.from_tuples([('Line', 'ab'), ('Line', 'ca'), ('Transformer', 'ca_t')]) + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'a') + n.add('Bus', 'b') + n.add('Bus', 'c') + n.add('Generator', 'hydro30', bus='a', p_nom=100, marginal_cost=10) + n.add('Generator', 'diesel30', bus='b', p_nom=100, marginal_cost=50) + n.add('Generator', 'peak30', bus='c', p_nom=100, marginal_cost=200) + n.add('Load', 'town30', bus='c', p_set=[40, 60, 80, 50]) + n.add('Line', 'ab', bus0='a', bus1='b', x=0.1, s_nom=60) + n.add('Line', 'bc', bus0='b', bus1='c', x=0.1, s_nom=60, s_max_pu=0.9) + n.add('Line', 'ca', bus0='c', bus1='a', x=0.1, s_nom_extendable=True, capital_cost=5, s_nom_max=200) + n.add('Transformer', 'ca_t', bus0='c', bus1='a', x=0.2, s_nom=30) + n.add('Transformer', 'bc_t', bus0='b', bus1='c', x=0.3, s_nom_extendable=True, capital_cost=3, s_nom_max=50) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2493,6 +2567,7 @@ data prep, or harness — is one open question. Line numbers are pinned pypsa | `NotImplementedError`, `global_constraints.py:457` | depletion with period weightings `!= 1` | out | | | `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` and `Transformer_loss_max` | X4 | | `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | +| `ValueError`, `abstract.py:427`, `:445` | a security-constrained run over scenarios | rows per scenario, not refused | | Duals and solutions are read back by the harness on the lpspec side: `marginal_price` is the balance dual over `w_objective`, `mu_upper` the @@ -2501,7 +2576,7 @@ concatenation of the regime blocks, `p0`/`p1` derived from `Link-p`. ## The file -A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. Which snapshots an asset is active in, and a scenario's weight, are data prep. +A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. #### Sets @@ -2518,9 +2593,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | | $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | | $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | -| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | -| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | +| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K}`$ — passive branches, each between two buses, their flow set by impedance | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | +| $`\mathcal{K}^{\mathrm{out}}`$ | index $`\kappa`$ — `outage` with $`\mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$ — the passive branches a security-constrained run takes out one at a time — PyPSA's `branch_outages`, each a line or a transformer; none on a plain run | | $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a passive branch's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | | $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | @@ -2699,6 +2775,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | | $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | | $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row | +| $`\beta`$ | `Line_BODF` over $`\mathcal{K} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a line takes on when that branch goes out — PyPSA's `BODF`, from the sub-network's PTDF, data prep; a row only where the line and the outage share a sub-network, -1 at the outaged line itself | | $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the same; false with no segments is a lossless run | | $`\overline{\ell}`$ | `Line_loss_max` over $`\mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | | $`\mathrm{a}`$ | `Line_loss_slope` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep | @@ -2712,6 +2789,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$ — a given nominal apparent power for an extendable transformer; one without a value has no row here | | $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | | $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row | +| $`\beta^{\sigma}`$ | `Transformer_BODF` over $`\mathcal{M} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage share a sub-network | | $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{M} \times \mathcal{C}`$ — a fixed transformer's phase shift in radians, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; zero for a varying transformer, whose shift is a decision instead, so the constant and the variable term never both count a shift. A transformer with no shift or in no cycle has no row | | $`\mathrm{Transformer\_phase\_shift\_varying}`$ | `Transformer_phase_shift_varying` over $`\mathcal{M}`$ — whether a transformer's phase shift is a decision — PyPSA's `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a fixed shift carried by `phase_shift` | | $`\mathrm{Transformer\_phase\_shift\_min}`$ | `Transformer_phase_shift_min` over $`\mathcal{M}`$ — the least a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_min`; where it is below `phase_shift_max` the shift is a decision, otherwise the transformer keeps its fixed `phase_shift` | @@ -2827,6 +2905,9 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | | $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. PyPSA sums the components that carry a carrier attribute; the transformer term is the spec's own extension, since PyPSA gives a transformer no carrier | +| $`\check{s}`$ | `Line_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the flow a line's post-contingency rows read — its flow where it stands, nothing where it does not, since PyPSA builds those rows for every branch of the sub-network in every snapshot | +| $`\check{\sigma}`$ | `Transformer_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — the flow a transformer's post-contingency rows read, as a line's | +| $`\hat{s}`$ | `Outage_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}^{\mathrm{out}}`$ — the flow an outage takes off its branch — the outaged line's or transformer's flow before it goes out | Upright is what the model is given — a parameter such as $`\mathrm{Transformer\_phase\_shift\_varying}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{Transformer\_phase\_shift}`$. An index is italic too, being what a quantifier chooses, and a set is script. @@ -5110,6 +5191,180 @@ Transformer_loss_tangents_reverse: \ell^{\sigma}_{\xi,t,m} - \mathrm{a}^{\sigma}_{t,m,b} \cdot \sigma_{\xi,t,m} \ge \mathrm{b}^{\sigma}_{t,m,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} ``` +### `Line-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` + +`Line_fix_s_lower_security` + +```yaml +Line_fix_s_lower_security: + description: >- + `Line-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, a fixed line carries at least the negative + of its rating: its flow takes on its share of the outaged branch's + flow. PyPSA names one row per outaged component `c` and + sub-network `n`; this block states them all over the outage + dimension + dims: [scenario, snapshot, line, outage] + where: not Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom +``` + +```math +\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +``` + +### `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` + +`Line_fix_s_upper_security` + +```yaml +Line_fix_s_upper_security: + description: >- + `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, a fixed line carries at most its rating: its + flow takes on its share of the outaged branch's flow. PyPSA names + one row per outaged component `c` and sub-network `n`; this block + states them all over the outage dimension + dims: [scenario, snapshot, line, outage] + where: not Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom +``` + +```math +\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +``` + +### `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` + +`Line_ext_s_lower_security` + +```yaml +Line_ext_s_lower_security: + description: >- + `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, an extendable line carries at least the + negative of its rating of the chosen build: its flow takes on its + share of the outaged branch's flow. PyPSA names one row per + outaged component `c` and sub-network `n`; this block states them + all over the outage dimension + dims: [scenario, snapshot, line, outage] + where: Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom_ext +``` + +```math +\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +``` + +### `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` + +`Line_ext_s_upper_security` + +```yaml +Line_ext_s_upper_security: + description: >- + `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, an extendable line carries at most its rating + of the chosen build: its flow takes on its share of the outaged + branch's flow. PyPSA names one row per outaged component `c` and + sub-network `n`; this block states them all over the outage + dimension + dims: [scenario, snapshot, line, outage] + where: Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom_ext +``` + +```math +\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +``` + +### `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` + +`Transformer_fix_s_lower_security` + +```yaml +Transformer_fix_s_lower_security: + description: >- + `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, a fixed transformer carries at least the + negative of its rating: its flow takes on its share of the outaged + branch's flow. PyPSA names one row per outaged component `c` and + sub-network `n`; this block states them all over the outage + dimension + dims: [scenario, snapshot, transformer, outage] + where: not Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom +``` + +```math +\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +``` + +### `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` + +`Transformer_fix_s_upper_security` + +```yaml +Transformer_fix_s_upper_security: + description: >- + `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, a fixed transformer carries at most its + rating: its flow takes on its share of the outaged branch's flow. + PyPSA names one row per outaged component `c` and sub-network `n`; + this block states them all over the outage dimension + dims: [scenario, snapshot, transformer, outage] + where: not Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom +``` + +```math +\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +``` + +### `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` + +`Transformer_ext_s_lower_security` + +```yaml +Transformer_ext_s_lower_security: + description: >- + `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, an extendable transformer carries at least + the negative of its rating of the chosen build: its flow takes on + its share of the outaged branch's flow. PyPSA names one row per + outaged component `c` and sub-network `n`; this block states them + all over the outage dimension + dims: [scenario, snapshot, transformer, outage] + where: Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext +``` + +```math +\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +``` + +### `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` + +`Transformer_ext_s_upper_security` + +```yaml +Transformer_ext_s_upper_security: + description: >- + `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, an extendable transformer carries at most + its rating of the chosen build: its flow takes on its share of the + outaged branch's flow. PyPSA names one row per outaged component + `c` and sub-network `n`; this block states them all over the + outage dimension + dims: [scenario, snapshot, transformer, outage] + where: Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext +``` + +```math +\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +``` + ### `Kirchhoff-Voltage-Law` `Kirchhoff_Voltage_Law` @@ -6806,6 +7061,56 @@ Carrier_additions: \mathit{Carrier\_additions}_{y,i} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} + \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} + \sum_{j \in \mathcal{J} \,:\, \mathrm{Process\_carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_carrier}(m) = i} \Sigma_{m} \cdot \mathrm{new}^{\sigma}_{y,m} \qquad \forall\, y \in \mathcal{Y},\ i \in \mathcal{I} ``` +### `Line_s_monitored` + +```yaml +Line_s_monitored: + description: >- + the flow a line's post-contingency rows read — its flow where it stands, + nothing where it does not, since PyPSA builds those rows for every + branch of the sub-network in every snapshot + dims: [scenario, snapshot, line] + cases: + standing: { when: Line_active, expression: Line_s } + otherwise: 0 +``` + +```math +\check{s}_{\xi,t,k} = \begin{cases} s_{\xi,t,k} & \text{if } \mathrm{on}^{s}_{t,k} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} +``` + +### `Transformer_s_monitored` + +```yaml +Transformer_s_monitored: + description: the flow a transformer's post-contingency rows read, as a line's + dims: [scenario, snapshot, transformer] + cases: + standing: { when: Transformer_active, expression: Transformer_s } + otherwise: 0 +``` + +```math +\check{\sigma}_{\xi,t,m} = \begin{cases} \sigma_{\xi,t,m} & \text{if } \mathrm{on}^{\sigma}_{t,m} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} +``` + +### `Outage_s` + +```yaml +Outage_s: + description: >- + the flow an outage takes off its branch — the outaged line's or + transformer's flow before it goes out + dims: [scenario, snapshot, outage] + cases: + line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line, over=line, into=outage)" } + otherwise: at(Transformer_s_monitored, by=Outage_transformer, over=transformer, into=outage) +``` + +```math +\hat{s}_{\xi,t,\kappa} = \begin{cases} \check{s}_{\xi,t,\mathrm{Outage\_line}(\kappa)} & \text{if } \mathrm{Outage\_line}(\kappa) \text{ is defined} \\ \check{\sigma}_{\xi,t,\mathrm{Outage\_transformer}(\kappa)} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ \kappa \in \mathcal{K}^{\mathrm{out}} +``` + #### Variable domains **`Generator_p`** diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 4f7efc54..9b0d747b 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -10,8 +10,10 @@ description: >- paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the - model collapses to the standard one. Which snapshots an asset is active in, - and a scenario's weight, are data prep. + model collapses to the standard one. A security-constrained run copies each + branch flow limit once per outage in an `outage` set that a plain run leaves + empty. Which snapshots an asset is active in, a scenario's weight, and the + outage factors are data prep. dimensions: scenario: @@ -49,6 +51,11 @@ dimensions: description: passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised cycle: description: independent cycles of the passive network graph — the cycle basis, data prep + outage: + description: >- + the passive branches a security-constrained run takes out one at a time + — PyPSA's `branch_outages`, each a line or a transformer; none on a + plain run segment: description: >- the cuts a passive branch's loss curve is held above — PyPSA's tangents, @@ -687,6 +694,13 @@ parameters: the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row dims: [line, cycle] + Line_BODF: + description: >- + the share of an outaged branch's flow a line takes on when that branch + goes out — PyPSA's `BODF`, from the sub-network's PTDF, data prep; a row + only where the line and the outage share a sub-network, -1 at the + outaged line itself + dims: [line, outage] transmission_losses: description: >- whether the network dissipates transmission losses — PyPSA's @@ -740,6 +754,12 @@ parameters: signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row dims: [transformer, cycle] + Transformer_BODF: + description: >- + the share of an outaged branch's flow a transformer takes on when that + branch goes out, as a line's; a row only where the transformer and the + outage share a sub-network + dims: [transformer, outage] Transformer_phase_shift_weight: description: >- a fixed transformer's phase shift in radians, signed by its orientation @@ -956,6 +976,14 @@ relations: description: the bus at a transformer's other end key: transformer values: bus + Outage_line: + description: the line an outage takes out; an outage of a transformer has no row + key: outage + values: line + Outage_transformer: + description: the transformer an outage takes out; an outage of a line has no row + key: outage + values: transformer variables: Generator_p: @@ -1623,6 +1651,29 @@ expressions: + sum(Line_s_nom_ext * Line_first_active, by=Line_carrier, over=line, into=carrier) + sum(Process_p_nom_ext * Process_first_active, by=Process_carrier, over=process, into=carrier) + sum(Transformer_s_nom_ext * Transformer_first_active, by=Transformer_carrier, over=transformer, into=carrier) + Line_s_monitored: + description: >- + the flow a line's post-contingency rows read — its flow where it stands, + nothing where it does not, since PyPSA builds those rows for every + branch of the sub-network in every snapshot + dims: [scenario, snapshot, line] + cases: + standing: { when: Line_active, expression: Line_s } + otherwise: 0 + Transformer_s_monitored: + description: the flow a transformer's post-contingency rows read, as a line's + dims: [scenario, snapshot, transformer] + cases: + standing: { when: Transformer_active, expression: Transformer_s } + otherwise: 0 + Outage_s: + description: >- + the flow an outage takes off its branch — the outaged line's or + transformer's flow before it goes out + dims: [scenario, snapshot, outage] + cases: + line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line, over=line, into=outage)" } + otherwise: at(Transformer_s_monitored, by=Outage_transformer, over=transformer, into=outage) constraints: Generator_fix_p_lower: @@ -2487,6 +2538,92 @@ constraints: dims: [scenario, snapshot, transformer, segment] where: transmission_losses AND Transformer_active expression: Transformer_loss - Transformer_loss_slope * Transformer_s >= Transformer_loss_offset + Line_fix_s_lower_security: + description: >- + `Line-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, a fixed line carries at least the negative + of its rating: its flow takes on its share of the outaged branch's + flow. PyPSA names one row per outaged component `c` and + sub-network `n`; this block states them all over the outage + dimension + dims: [scenario, snapshot, line, outage] + where: not Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom + Line_fix_s_upper_security: + description: >- + `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, a fixed line carries at most its rating: its + flow takes on its share of the outaged branch's flow. PyPSA names + one row per outaged component `c` and sub-network `n`; this block + states them all over the outage dimension + dims: [scenario, snapshot, line, outage] + where: not Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom + Line_ext_s_lower_security: + description: >- + `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, an extendable line carries at least the + negative of its rating of the chosen build: its flow takes on its + share of the outaged branch's flow. PyPSA names one row per + outaged component `c` and sub-network `n`; this block states them + all over the outage dimension + dims: [scenario, snapshot, line, outage] + where: Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom_ext + Line_ext_s_upper_security: + description: >- + `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` — + after any one outage, an extendable line carries at most its rating + of the chosen build: its flow takes on its share of the outaged + branch's flow. PyPSA names one row per outaged component `c` and + sub-network `n`; this block states them all over the outage + dimension + dims: [scenario, snapshot, line, outage] + where: Line_s_nom_extendable AND Line_BODF + expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom_ext + Transformer_fix_s_lower_security: + description: >- + `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, a fixed transformer carries at least the + negative of its rating: its flow takes on its share of the outaged + branch's flow. PyPSA names one row per outaged component `c` and + sub-network `n`; this block states them all over the outage + dimension + dims: [scenario, snapshot, transformer, outage] + where: not Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom + Transformer_fix_s_upper_security: + description: >- + `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, a fixed transformer carries at most its + rating: its flow takes on its share of the outaged branch's flow. + PyPSA names one row per outaged component `c` and sub-network `n`; + this block states them all over the outage dimension + dims: [scenario, snapshot, transformer, outage] + where: not Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom + Transformer_ext_s_lower_security: + description: >- + `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, an extendable transformer carries at least + the negative of its rating of the chosen build: its flow takes on + its share of the outaged branch's flow. PyPSA names one row per + outaged component `c` and sub-network `n`; this block states them + all over the outage dimension + dims: [scenario, snapshot, transformer, outage] + where: Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + Transformer_ext_s_upper_security: + description: >- + `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` + — after any one outage, an extendable transformer carries at most + its rating of the chosen build: its flow takes on its share of the + outaged branch's flow. PyPSA names one row per outaged component + `c` and sub-network `n`; this block states them all over the + outage dimension + dims: [scenario, snapshot, transformer, outage] + where: Transformer_s_nom_extendable AND Transformer_BODF + expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext Kirchhoff_Voltage_Law: description: >- `Kirchhoff-Voltage-Law` — around every independent cycle the diff --git a/examples/references/pypsa/reference.py b/examples/references/pypsa/reference.py index 0efedf47..0c70a779 100644 --- a/examples/references/pypsa/reference.py +++ b/examples/references/pypsa/reference.py @@ -16,7 +16,8 @@ the spine (`spine.py`) plus that rung's own `n.add` calls, data inline, so the PyPSA model under review is the script itself. A rung stated by a file of its own names it as `MODEL`; one that needs `n.optimize` keywords names them -as `OPTIMIZE`. PyPSA is not a dependency +as `OPTIMIZE`; one that names `BRANCH_OUTAGES` is solved by +`n.optimize.optimize_security_constrained` over them. PyPSA is not a dependency of this project; this script pins the versions the recorded numbers are from, and the `PyPSA references` workflow runs `--check` on every change. """ @@ -67,7 +68,13 @@ def record(n: pypsa.Network) -> dict[str, object]: def solved(rung: str) -> dict[str, object]: n = build(rung) - status, condition = n.optimize(solver_name='highs', **keywords(rung)) + outages = getattr(importlib.import_module(rung), 'BRANCH_OUTAGES', None) + if outages is None: + status, condition = n.optimize(solver_name='highs', **keywords(rung)) + else: + status, condition = n.optimize.optimize_security_constrained( + solver_name='highs', branch_outages=outages, **keywords(rung) + ) assert status == 'ok', f'{rung}: HiGHS did not solve — {status} / {condition}' return record(n) diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 62204118..62698829 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1105,5 +1105,55 @@ "Store-fix-e-lower": 16, "Store-fix-e-upper": 16 } + }, + "rung_30_security_constrained": { + "columns": { + "Generator-p": 20, + "Line-s": 12, + "Line-s_nom": 1, + "Link-p": 4, + "Transformer-s": 8, + "Transformer-s_nom": 1 + }, + "global_constraints": {}, + "objective": 22113.333333333332, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 20, + "Generator-fix-p-lower": 20, + "Generator-fix-p-upper": 20, + "Kirchhoff-Voltage-Law": 8, + "Line-ext-s-lower": 4, + "Line-ext-s-lower-security-for-Line-outage-in-sub-network-2": 8, + "Line-ext-s-lower-security-for-Transformer-outage-in-sub-network-2": 4, + "Line-ext-s-upper": 4, + "Line-ext-s-upper-security-for-Line-outage-in-sub-network-2": 8, + "Line-ext-s-upper-security-for-Transformer-outage-in-sub-network-2": 4, + "Line-ext-s_nom-lower": 1, + "Line-ext-s_nom-upper": 1, + "Line-fix-s-lower": 8, + "Line-fix-s-lower-security-for-Line-outage-in-sub-network-2": 16, + "Line-fix-s-lower-security-for-Transformer-outage-in-sub-network-2": 8, + "Line-fix-s-upper": 8, + "Line-fix-s-upper-security-for-Line-outage-in-sub-network-2": 16, + "Line-fix-s-upper-security-for-Transformer-outage-in-sub-network-2": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Transformer-ext-s-lower": 4, + "Transformer-ext-s-lower-security-for-Line-outage-in-sub-network-2": 8, + "Transformer-ext-s-lower-security-for-Transformer-outage-in-sub-network-2": 4, + "Transformer-ext-s-upper": 4, + "Transformer-ext-s-upper-security-for-Line-outage-in-sub-network-2": 8, + "Transformer-ext-s-upper-security-for-Transformer-outage-in-sub-network-2": 4, + "Transformer-ext-s_nom-lower": 1, + "Transformer-ext-s_nom-upper": 1, + "Transformer-fix-s-lower": 4, + "Transformer-fix-s-lower-security-for-Line-outage-in-sub-network-2": 8, + "Transformer-fix-s-lower-security-for-Transformer-outage-in-sub-network-2": 4, + "Transformer-fix-s-upper": 4, + "Transformer-fix-s-upper-security-for-Line-outage-in-sub-network-2": 8, + "Transformer-fix-s-upper-security-for-Transformer-outage-in-sub-network-2": 4 + } } } diff --git a/examples/references/pypsa/rung_30_security_constrained.py b/examples/references/pypsa/rung_30_security_constrained.py new file mode 100644 index 00000000..31a434e5 --- /dev/null +++ b/examples/references/pypsa/rung_30_security_constrained.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 30: security-constrained — a meshed triangle of lines and two transformers, one extendable, that must carry their flow within their rating after any one of three outages.""" + +from __future__ import annotations + +import pandas as pd +import spine + +BRANCH_OUTAGES = pd.MultiIndex.from_tuples([('Line', 'ab'), ('Line', 'ca'), ('Transformer', 'ca_t')]) + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'a') + n.add('Bus', 'b') + n.add('Bus', 'c') + n.add('Generator', 'hydro30', bus='a', p_nom=100, marginal_cost=10) + n.add('Generator', 'diesel30', bus='b', p_nom=100, marginal_cost=50) + n.add('Generator', 'peak30', bus='c', p_nom=100, marginal_cost=200) + n.add('Load', 'town30', bus='c', p_set=[40, 60, 80, 50]) + n.add('Line', 'ab', bus0='a', bus1='b', x=0.1, s_nom=60) + n.add('Line', 'bc', bus0='b', bus1='c', x=0.1, s_nom=60, s_max_pu=0.9) + n.add('Line', 'ca', bus0='c', bus1='a', x=0.1, s_nom_extendable=True, capital_cost=5, s_nom_max=200) + n.add('Transformer', 'ca_t', bus0='c', bus1='a', x=0.2, s_nom=30) + n.add('Transformer', 'bc_t', bus0='b', bus1='c', x=0.3, s_nom_extendable=True, capital_cost=3, s_nom_max=50) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 6eafe555..2f2eb235 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -19,6 +19,7 @@ dimensions: process: { index: j, set: '\mathcal{J}' } process_output: { index: r, set: '\mathcal{R}' } cycle: { index: c, set: '\mathcal{C}' } + outage: { index: '\kappa', set: '\mathcal{K}^{\mathrm{out}}' } scenario: { index: '\xi', set: '\Xi' } period: { index: y, set: '\mathcal{Y}' } carrier: { index: i, set: '\mathcal{I}' } @@ -210,6 +211,11 @@ names: Transformer_loss_max: '\overline{\ell}^{\sigma}' Transformer_loss_slope: '\mathrm{a}^{\sigma}' Transformer_loss_offset: '\mathrm{b}^{\sigma}' + Line_BODF: '\beta' + Transformer_BODF: '\beta^{\sigma}' + Line_s_monitored: '\check{s}' + Transformer_s_monitored: '\check{\sigma}' + Outage_s: '\hat{s}' GlobalConstraint_type: '\mathrm{type}' GlobalConstraint_sense: '\mathrm{sense}' GlobalConstraint_constant: '\mathrm{K}' From 51096860a2139bc91c5b9c56e892033e56b127ce Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 13:03:54 +0200 Subject: [PATCH 16/46] docs(pypsa): an mga flag caps the system cost at a budget and minimises weighted builds --- docs/examples/pypsa.md | 184 +++++++++++++++++++--- examples/pypsa.yaml | 104 ++++++++++-- examples/references/pypsa/reference.py | 13 +- examples/references/pypsa/references.json | 23 +++ examples/references/pypsa/rung_31_mga.py | 30 ++++ examples/symbols/pypsa.yaml | 9 ++ tests/test_pypsa_collapses.py | 19 ++- 7 files changed, 336 insertions(+), 46 deletions(-) create mode 100644 examples/references/pypsa/rung_31_mga.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 5788a998..580bb1a5 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -86,8 +86,8 @@ def build(): | [`Link-fix-p-upper`](#link-fix-p-upper) | done | | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | a loaded bus with nothing attached: PyPSA refuses, see X2 | | `Bus-meshed-*-nodal_balance` | out | the same balance rows, dealt into linopy containers by how many component columns name a bus — `meshed_thresholds`, an `n.optimize()` keyword defaulting to `[30, 100, 400]`. Same rows, same duals, another name; a modeler whose engine wants the split states it, the file does not (#123) | -| [`marginal_cost`](#objective) | done | | -| [`marginal_cost_quadratic`](#objective) | done | rung 10, below; Generator and Link — PyPSA also carries it on storage units and stores, one more term each of the same shape | +| [`marginal_cost`](#system_cost) | done | | +| [`marginal_cost_quadratic`](#system_cost) | done | rung 10, below; Generator and Link — PyPSA also carries it on storage units and stores, one more term each of the same shape | | `objective_constant` | split | an objective shift, compared net of `n._objective_constant` — rungs 11 and 13 carry a nonzero one, `21915277.52` and `160.0`, so the netting is under test | @@ -129,7 +129,7 @@ def build(): | [`StorageUnit-energy_balance`](#storageunit-energy_balance) | done | the charge carried into a snapshot is a cased quantity — cyclic, opening, carried; `(1-loss)**eh` is prep | | [`Store-energy_balance`](#store-energy_balance) | done | same | | [`StorageUnit-p_set`](#storageunit-p_set), [`{c}-{attr}_set`](#generator-p_set) | done | `Generator-p_set`, `Link-p_set`, `StorageUnit-state_of_charge_set`, `Store-e_set`, `Line-s_set` | -| [`marginal_cost_storage`, `spill_cost`](#objective) | done | | +| [`marginal_cost_storage`, `spill_cost`](#system_cost) | done | | > ✔ `pypsa 1.3.0` solves this rung's network at objective `4456.659315422356`, 103 rows. @@ -203,7 +203,7 @@ def build(): | [`{c}-ext-p_nom-lower/upper`](#generator-ext-p_nom-lower) | done | | | [`{c}-p_nom_set`](#generator-p_nom_set) | done | | | [`Generator-e_sum_min/max`](#generator-e_sum_min) | done | | -| [capital cost](#objective) | done | `periodized_cost` is an annuity, data prep | +| [capital cost](#system_cost) | done | `periodized_cost` is an annuity, data prep | > ✔ `pypsa 1.3.0` solves this rung's network at objective `7633.908502024292`, 184 rows. @@ -604,7 +604,7 @@ def build(): | [`{c}-com-up-time`, `-down-time`](#generator-com-up-time) | done | `sum_back(window=min_up_time)` | | [`{c}-com-status-min_up_time_must_stay_up`](#generator-com-status-min_up_time_must_stay_up) | done | the window is a prep mask — `position()` takes a literal | | [`{c}-com-status-min_down_time_must_stay_up`](#generator-com-status-min_down_time_must_stay_up) | done | the same prep mask over the down time brought in, status zero; PyPSA's name says `_must_stay_up`; rung 24 records it | -| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#system_cost) | done | | | [`{c}-com-p-before/-current/-partly-*`](pypsa_linearized_uc.md) | done | rung 12, a file of its own | @@ -823,7 +823,7 @@ feeds zero, so the term vanishes and the objective stays linear. | PyPSA | status | note | | --- | --- | --- | -| [`marginal_cost_quadratic`](#objective) | done | degree 2 in the objective; Generator and Link here | +| [`marginal_cost_quadratic`](#system_cost) | done | degree 2 in the objective; Generator and Link here | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12587.437500000098`, 60 rows. @@ -1304,7 +1304,7 @@ excess per scenario and the tail's average, blended into the objective at | [`CVaR-a`, `CVaR-theta`, `CVaR`](#variable-domains) | done | | | [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension | | [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep | -| [objective](#objective) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | +| [objective](#system_cost) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | > ✔ `pypsa 1.3.0` solves this rung's network at objective `9267.386666666665`, 87 rows. @@ -1357,7 +1357,7 @@ own dimensions. | [`Generator-p`](#variable-domains) | done | where the generator stands in the snapshot's period — `active`, data prep | | [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | | [`Carrier-growth_limit`](#carrier-growth_limit) | done | every extendable component of the carrier, counted in the first period a build stands in; `edge=0` at the first period | -| [objective](#objective) | done | period weight on operation; capacity once per period it stands in | +| [objective](#system_cost) | done | period weight on operation; capacity once per period it stands in | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | @@ -1548,7 +1548,7 @@ generator's and the link's, read over a converter. | [`Process-p_set`](#process-p_set) | done | a fixed internal power schedule | | [`Process-p_nom_set`](#process-p_nom_set) | done | a fixed built capacity | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | each port enters at its `rate` | -| [objective](#objective) | done | marginal cost on internal power; capital on capacity | +| [objective](#system_cost) | done | marginal cost on internal power; capital on capacity | > ✔ `pypsa 1.3.0` solves this rung's network at objective `9730.0`, 70 rows. @@ -1624,7 +1624,7 @@ transformer's built capacity. | [`Transformer-s_nom_set`](#transformer-s_nom_set) | done | a fixed built capacity | | [`Kirchhoff-Voltage-Law`](#kirchhoff-voltage-law) | done | rung 6, over `x_pu_eff` and a phase shift | | [`Transformer-phase_shift`](#variable-domains) | done | rung 20, an optimised phase shift | -| [objective](#objective) | done | capital on capacity | +| [objective](#system_cost) | done | capital on capacity | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12274.401472395122`, 106 rows. @@ -2047,7 +2047,7 @@ extendable, modular, or both. | [`Link-com-status-min_up_time_must_stay_up`, `-min_down_time_must_stay_up`](#link-com-status-min_up_time_must_stay_up) | done | prep masks, as the generator's | | [`Link-p-ramp_limit_*`, `-*-bigM`](#link-p-ramp_limit_up) | done | the generator's cased allowance and big-M rows over flow | | [`Link-p_nom_modularity`](#link-p_nom_modularity) | done | | -| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#system_cost) | done | | > ✔ `pypsa 1.3.0` solves this rung's network at objective `14013.0`, 235 rows. @@ -2169,7 +2169,7 @@ time rules bind. | [`Process-com-status-min_up_time_must_stay_up`, `-min_down_time_must_stay_up`](#process-com-status-min_up_time_must_stay_up) | done | prep masks, as the generator's | | [`Process-p-ramp_limit_*`, `-*-bigM`](#process-p-ramp_limit_up) | done | the generator's cased allowance and big-M rows over internal power | | [`Process-p_nom_modularity`](#process-p_nom_modularity) | done | | -| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#system_cost) | done | | > ✔ `pypsa 1.3.0` solves this rung's network at objective `15956.125`, 235 rows. @@ -2552,6 +2552,77 @@ def build(): +### Rung 31 — MGA + +`optimize_mga` needs a solved network. It builds the model again, adds one +`budget` row, and replaces the objective (`mga.py:343`, `:350`, `:372`). The row +caps the model's objective plus `installed_capex` at `(1 + slack)` times +`capex + opex` of the optimum (`mga.py:256`, `:270`). Net of the objective +constant, that is `system_cost <= mga_budget`, and data prep computes the +budget from the optimum. The slack also scales the capital cost of fixed +assets, which the objective does not carry. In this rung, the 45 of fixed +hydro capex adds 4.5 to the cap. The new objective is the weights times the +variables they name, times `-1` to maximise (`mga.py:187`, `:372`). +`optimize_mga_in_direction` builds the same row and minimises the negated sum +of direction times dimension weights (`mga.py:508`, `:513`). Data prep folds +the sense and the direction into `{c}_mga_weight`, so one `goal` case covers +both. + +This rung maximises the wind build at a slack of 0.1. The optimum costs +`9884.17` and builds 116.67 of wind. The MGA run builds 174.85, and the +`budget` row holds with equality at dual `-0.05`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`budget`](#budget) | done | the `mga` flag builds the row; `mga_budget` is `(1 + slack) * optimal_cost - fixed_cost` plus the objective constant, data prep. The multi-period and stochastic budgets are PyPSA's `statistics` sums, data prep, and no rung records them | +| [MGA objective](#goal) | done | `goal` is the weighted builds where `mga` holds and `system_cost` elsewhere; a plain run is the standard objective | +| `weights` on a variable other than a build | open | PyPSA takes any `{c}-{attr}`, for example `Generator-p` per snapshot; each is one more parameter and one more term of the same shape | +| `optimize_mga_in_multiple_directions`, `project_solved` | out | a loop of `optimize_mga_in_direction` runs, and a read-back of their solutions | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `-174.8541663000001`, 63 rows. + +
+The network, as PyPSA code + +`rung_31_mga.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 31: MGA — the most wind a network builds within a tenth above its least cost.""" + +from __future__ import annotations + +import spine + +MGA = {'weights': {'Generator': {'p_nom': {'wind31': 1}}}, 'sense': 'max', 'slack': 0.1} + + +def build(): + """The spine plus a bus that extendable wind and gas and a fixed hydro unit with a capital cost serve; the wind carries a build already, so the objective has a constant.""" + n = spine.build() + n.add('Bus', 'mga31') + n.add( + 'Generator', + 'wind31', + bus='mga31', + p_nom=10, + p_nom_extendable=True, + capital_cost=20, + p_max_pu=[0.3, 0.9, 0.5, 0.6], + ) + n.add('Generator', 'gas31', bus='mga31', p_nom_extendable=True, capital_cost=8, marginal_cost=40) + n.add('Generator', 'hydro31', bus='mga31', p_nom=15, capital_cost=3, marginal_cost=5) + n.add('Load', 'mga31_load', bus='mga31', p_set=[50, 80, 60, 70]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2576,7 +2647,7 @@ concatenation of the regime blocks, `p0`/`p1` derived from `Link-p`. ## The file -A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. +A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. Under the `mga` flag the file is PyPSA's `optimize_mga`: the system cost is capped at a budget above its optimum, and weighted builds are the objective instead. #### Sets @@ -2690,6 +2761,15 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | | $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor | +| $`\mathrm{mga}`$ | `mga` (scalar) — whether the run is PyPSA's `optimize_mga` or `optimize_mga_in_direction` — the system cost is capped by the `budget` row and the weighted builds are the objective; false is a plain `n.optimize()` | +| $`\overline{\mathrm{C}}`$ | `mga_budget` (scalar) — the most the system cost may reach under MGA — PyPSA's `(1 + slack) * optimal_cost - fixed_cost`, from the solved optimum's statistics, plus the objective constant this file leaves out of the cost; data prep | +| $`\beta`$ | `Generator_mga_weight` over $`\mathcal{G}`$ — what one unit of a generator's build weighs in the MGA objective — PyPSA's `weights['Generator']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | +| $`\beta^{f}`$ | `Link_mga_weight` over $`\mathcal{L}`$ — what one unit of a link's build weighs in the MGA objective — PyPSA's `weights['Link']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | +| $`\beta^{h}`$ | `StorageUnit_mga_weight` over $`\mathcal{S}`$ — what one unit of a storage unit's build weighs in the MGA objective — PyPSA's `weights['StorageUnit']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | +| $`\beta^{e}`$ | `Store_mga_weight` over $`\mathcal{V}`$ — what one unit of a store's build weighs in the MGA objective — PyPSA's `weights['Store']['e_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | +| $`\beta^{s}`$ | `Line_mga_weight` over $`\mathcal{K}`$ — what one unit of a line's build weighs in the MGA objective — PyPSA's `weights['Line']['s_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | +| $`\beta^{z}`$ | `Process_mga_weight` over $`\mathcal{J}`$ — what one unit of a process's build weighs in the MGA objective — PyPSA's `weights['Process']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | +| $`\beta^{\sigma}`$ | `Transformer_mga_weight` over $`\mathcal{M}`$ — what one unit of a transformer's build weighs in the MGA objective — PyPSA's `weights['Transformer']['s_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | | $`\mathrm{w}^{y}`$ | `period_weight_objective` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.objective` — what a period's cost weighs | | $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep | | $`\mathrm{on}^{f}`$ | `Link_active` over $`\mathcal{T} \times \mathcal{L}`$ — whether a link stands in a snapshot's period — PyPSA's `active`, data prep | @@ -2908,6 +2988,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\check{s}`$ | `Line_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the flow a line's post-contingency rows read — its flow where it stands, nothing where it does not, since PyPSA builds those rows for every branch of the sub-network in every snapshot | | $`\check{\sigma}`$ | `Transformer_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — the flow a transformer's post-contingency rows read, as a line's | | $`\hat{s}`$ | `Outage_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}^{\mathrm{out}}`$ — the flow an outage takes off its branch — the outaged line's or transformer's flow before it goes out | +| $`\mathit{system\_cost}`$ | `system_cost` (scalar) — what `n.optimize()` minimises — capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail | +| $`\mathit{goal}`$ | `goal` (scalar) — what the run minimises — the system cost, or under MGA the weighted builds within the budget | Upright is what the model is given — a parameter such as $`\mathrm{Transformer\_phase\_shift\_varying}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{Transformer\_phase\_shift}`$. An index is italic too, being what a quantifier chooses, and a set is script. @@ -2926,21 +3008,12 @@ $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ counts within the group a relation pu ```yaml objective: sense: minimize - description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail - expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) - + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) - + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) - + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) - + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) - + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) - + CVaR_omega * CVaR + description: the system cost; under MGA the weighted builds instead + expression: goal ``` ```math -\min \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{c}^{\mathrm{cap},z}_{j} \cdot \mathrm{W}^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \mathrm{c}^{\mathrm{cap},\sigma}_{m} \cdot \mathrm{W}^{\sigma}_{m} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR +\min \mathit{goal} ``` ### `Generator-fix-p-lower` @@ -6269,6 +6342,22 @@ CVaR_def: \theta + \mathrm{v} \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot a_{\xi} \right) \le CVaR ``` +### `budget` + +`budget` + +```yaml +budget: + description: "`budget` — under MGA, the system cost stays within the slack above its optimum" + dims: [] + where: mga + expression: system_cost <= mga_budget +``` + +```math +\mathit{system\_cost} \le \overline{\mathrm{C}} \qquad \text{where } \mathrm{mga} +``` + ### `Generator_previous_status` ```yaml @@ -7111,6 +7200,51 @@ Outage_s: \hat{s}_{\xi,t,\kappa} = \begin{cases} \check{s}_{\xi,t,\mathrm{Outage\_line}(\kappa)} & \text{if } \mathrm{Outage\_line}(\kappa) \text{ is defined} \\ \check{\sigma}_{\xi,t,\mathrm{Outage\_transformer}(\kappa)} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ \kappa \in \mathcal{K}^{\mathrm{out}} ``` +### `system_cost` + +```yaml +system_cost: + description: what `n.optimize()` minimises — capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail + expression: >- + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + CVaR_omega * CVaR +``` + +```math +\mathit{system\_cost} = \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{c}^{\mathrm{cap},z}_{j} \cdot \mathrm{W}^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \mathrm{c}^{\mathrm{cap},\sigma}_{m} \cdot \mathrm{W}^{\sigma}_{m} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR +``` + +### `goal` + +```yaml +goal: + description: what the run minimises — the system cost, or under MGA the weighted builds within the budget + dims: [] + cases: + mga: + when: mga + expression: >- + sum(Generator_p_nom_ext * Generator_mga_weight) + + sum(Link_p_nom_ext * Link_mga_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_mga_weight) + + sum(Store_e_nom_ext * Store_mga_weight) + + sum(Line_s_nom_ext * Line_mga_weight) + + sum(Process_p_nom_ext * Process_mga_weight) + + sum(Transformer_s_nom_ext * Transformer_mga_weight) + otherwise: system_cost +``` + +```math +\mathit{goal} = \begin{cases} \sum_{g \in \mathcal{G}} P_{g} \cdot \beta_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \beta^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \beta^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \beta^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \beta^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \beta^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \beta^{\sigma}_{m} & \text{if } \mathrm{mga} \\ \mathit{system\_cost} & \text{otherwise} \end{cases} +``` + #### Variable domains **`Generator_p`** diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 9b0d747b..3d853972 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -13,7 +13,9 @@ description: >- model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the - outage factors are data prep. + outage factors are data prep. Under the `mga` flag the file is PyPSA's + `optimize_mga`: the system cost is capped at a budget above its optimum, and + weighted builds are the objective instead. dimensions: scenario: @@ -407,6 +409,61 @@ parameters: CVaR_inv_tail: description: PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor dims: [] + mga: + description: >- + whether the run is PyPSA's `optimize_mga` or `optimize_mga_in_direction` + — the system cost is capped by the `budget` row and the weighted builds + are the objective; false is a plain `n.optimize()` + dims: [] + dtype: bool + mga_budget: + description: >- + the most the system cost may reach under MGA — PyPSA's `(1 + slack) * + optimal_cost - fixed_cost`, from the solved optimum's statistics, plus + the objective constant this file leaves out of the cost; data prep + dims: [] + Generator_mga_weight: + description: >- + what one unit of a generator's build weighs in the MGA objective — PyPSA's + `weights['Generator']['p_nom']`, negated to maximise; a direction's weighted + dimensions fold into it, data prep + dims: [generator] + Link_mga_weight: + description: >- + what one unit of a link's build weighs in the MGA objective — PyPSA's + `weights['Link']['p_nom']`, negated to maximise; a direction's weighted + dimensions fold into it, data prep + dims: [link] + StorageUnit_mga_weight: + description: >- + what one unit of a storage unit's build weighs in the MGA objective — PyPSA's + `weights['StorageUnit']['p_nom']`, negated to maximise; a direction's weighted + dimensions fold into it, data prep + dims: [storage_unit] + Store_mga_weight: + description: >- + what one unit of a store's build weighs in the MGA objective — PyPSA's + `weights['Store']['e_nom']`, negated to maximise; a direction's weighted + dimensions fold into it, data prep + dims: [store] + Line_mga_weight: + description: >- + what one unit of a line's build weighs in the MGA objective — PyPSA's + `weights['Line']['s_nom']`, negated to maximise; a direction's weighted + dimensions fold into it, data prep + dims: [line] + Process_mga_weight: + description: >- + what one unit of a process's build weighs in the MGA objective — PyPSA's + `weights['Process']['p_nom']`, negated to maximise; a direction's weighted + dimensions fold into it, data prep + dims: [process] + Transformer_mga_weight: + description: >- + what one unit of a transformer's build weighs in the MGA objective — PyPSA's + `weights['Transformer']['s_nom']`, negated to maximise; a direction's weighted + dimensions fold into it, data prep + dims: [transformer] period_weight_objective: description: PyPSA's `investment_period_weightings.objective` — what a period's cost weighs dims: [period] @@ -1674,6 +1731,33 @@ expressions: cases: line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line, over=line, into=outage)" } otherwise: at(Transformer_s_monitored, by=Outage_transformer, over=transformer, into=outage) + system_cost: + description: what `n.optimize()` minimises — capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail + expression: >- + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + CVaR_omega * CVaR + goal: + description: what the run minimises — the system cost, or under MGA the weighted builds within the budget + dims: [] + cases: + mga: + when: mga + expression: >- + sum(Generator_p_nom_ext * Generator_mga_weight) + + sum(Link_p_nom_ext * Link_mga_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_mga_weight) + + sum(Store_e_nom_ext * Store_mga_weight) + + sum(Line_s_nom_ext * Line_mga_weight) + + sum(Process_p_nom_ext * Process_mga_weight) + + sum(Transformer_s_nom_ext * Transformer_mga_weight) + otherwise: system_cost constraints: Generator_fix_p_lower: @@ -2978,17 +3062,13 @@ constraints: description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" dims: [] expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR + budget: + description: "`budget` — under MGA, the system cost stays within the slack above its optimum" + dims: [] + where: mga + expression: system_cost <= mga_budget objective: sense: minimize - description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail - expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) - + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) - + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) - + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) - + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) - + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) - + CVaR_omega * CVaR + description: the system cost; under MGA the weighted builds instead + expression: goal diff --git a/examples/references/pypsa/reference.py b/examples/references/pypsa/reference.py index 0c70a779..b8df1f91 100644 --- a/examples/references/pypsa/reference.py +++ b/examples/references/pypsa/reference.py @@ -17,7 +17,9 @@ the PyPSA model under review is the script itself. A rung stated by a file of its own names it as `MODEL`; one that needs `n.optimize` keywords names them as `OPTIMIZE`; one that names `BRANCH_OUTAGES` is solved by -`n.optimize.optimize_security_constrained` over them. PyPSA is not a dependency +`n.optimize.optimize_security_constrained` over them. A rung that names `MGA` +is solved once more through `n.optimize.optimize_mga` with those keywords, and +that solve is the record. PyPSA is not a dependency of this project; this script pins the versions the recorded numbers are from, and the `PyPSA references` workflow runs `--check` on every change. """ @@ -46,9 +48,9 @@ def build(rung: str) -> pypsa.Network: return importlib.import_module(rung).build() -def keywords(rung: str) -> dict[str, object]: - """What the rung's `n.optimize` takes beyond the solver — the script's `OPTIMIZE`, if it names any.""" - return dict(getattr(importlib.import_module(rung), 'OPTIMIZE', {})) +def keywords(rung: str, call: str = 'OPTIMIZE') -> dict[str, object]: + """What the rung's `n.optimize` (or `optimize_mga`, for `MGA`) takes beyond the solver — the script's `call`, if it names one.""" + return dict(getattr(importlib.import_module(rung), call, {})) def record(n: pypsa.Network) -> dict[str, object]: @@ -76,6 +78,9 @@ def solved(rung: str) -> dict[str, object]: solver_name='highs', branch_outages=outages, **keywords(rung) ) assert status == 'ok', f'{rung}: HiGHS did not solve — {status} / {condition}' + if mga := keywords(rung, 'MGA'): + status, condition = n.optimize.optimize_mga(solver_name='highs', **mga) + assert status == 'ok', f'{rung}: HiGHS did not solve the MGA run — {status} / {condition}' return record(n) diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 62698829..398e7161 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1155,5 +1155,28 @@ "Transformer-fix-s-upper-security-for-Line-outage-in-sub-network-2": 8, "Transformer-fix-s-upper-security-for-Transformer-outage-in-sub-network-2": 4 } + }, + "rung_31_mga": { + "columns": { + "Generator-p": 20, + "Generator-p_nom": 2, + "Link-p": 4, + "objective_constant": 1 + }, + "global_constraints": {}, + "objective": -174.8541663000001, + "objective_constant": 200.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-ext-p-lower": 8, + "Generator-ext-p-upper": 8, + "Generator-ext-p_nom-lower": 2, + "Generator-fix-p-lower": 12, + "Generator-fix-p-upper": 12, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "budget": 1 + } } } diff --git a/examples/references/pypsa/rung_31_mga.py b/examples/references/pypsa/rung_31_mga.py new file mode 100644 index 00000000..55bd8522 --- /dev/null +++ b/examples/references/pypsa/rung_31_mga.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 31: MGA — the most wind a network builds within a tenth above its least cost.""" + +from __future__ import annotations + +import spine + +MGA = {'weights': {'Generator': {'p_nom': {'wind31': 1}}}, 'sense': 'max', 'slack': 0.1} + + +def build(): + """The spine plus a bus that extendable wind and gas and a fixed hydro unit with a capital cost serve; the wind carries a build already, so the objective has a constant.""" + n = spine.build() + n.add('Bus', 'mga31') + n.add( + 'Generator', + 'wind31', + bus='mga31', + p_nom=10, + p_nom_extendable=True, + capital_cost=20, + p_max_pu=[0.3, 0.9, 0.5, 0.6], + ) + n.add('Generator', 'gas31', bus='mga31', p_nom_extendable=True, capital_cost=8, marginal_cost=40) + n.add('Generator', 'hydro31', bus='mga31', p_nom=15, capital_cost=3, marginal_cost=5) + n.add('Load', 'mga31_load', bus='mga31', p_set=[50, 80, 60, 70]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 2f2eb235..790e06c9 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -295,6 +295,15 @@ names: Line_capital_weight: '\mathrm{W}^{s}' Process_capital_weight: '\mathrm{W}^{z}' Transformer_capital_weight: '\mathrm{W}^{\sigma}' + mga: '\mathrm{mga}' + mga_budget: '\overline{\mathrm{C}}' + Generator_mga_weight: '\beta' + Link_mga_weight: '\beta^{f}' + StorageUnit_mga_weight: '\beta^{h}' + Store_mga_weight: '\beta^{e}' + Line_mga_weight: '\beta^{s}' + Process_mga_weight: '\beta^{z}' + Transformer_mga_weight: '\beta^{\sigma}' Generator_first_active: '\mathrm{new}' Link_first_active: '\mathrm{new}^{f}' StorageUnit_first_active: '\mathrm{new}^{h}' diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index 344a8561..2350e557 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -5,8 +5,9 @@ """`examples/pypsa.yaml` is the superset that collapses to the standard PyPSA model. The file always declares a `scenario` axis, a mask-only `period` axis, a `carrier` -axis and the CVaR rows. Fed one scenario, one period, all-active masks and unit -weights, every addition is a no-op and the standard model returns. The repository +axis, the CVaR rows and the MGA `budget` row. Fed one scenario, one period, +all-active masks, unit weights and a false `mga` flag, every addition is a no-op +and the standard model returns. The repository runs no solver, so the reduction is guarded structurally: `tests/fixtures/ pypsa_standard_shape.yaml` freezes the standard model's names and frames, and the only frame change a standard row may carry is a leading `scenario`. @@ -93,8 +94,16 @@ def test_the_extra_axes_and_rows_are_declared(): assert 'scenario_opex' in ALL.expressions, 'the per-scenario operating cost is a named expression' -def test_omega_blends_expectation_and_tail_in_the_objective(): - expr = ALL.objective.expression +def test_omega_blends_expectation_and_tail_in_the_system_cost(): + expr = ALL.expressions['system_cost'].expression assert all(term in expr for term in ('CVaR_omega', 'scenario_weight', 'scenario_opex', 'CVaR')), ( - 'the objective prices expected opex by scenario weight and blends the tail by omega' + 'the system cost prices expected opex by scenario weight and blends the tail by omega' ) + + +def test_a_run_without_the_mga_flag_minimises_the_system_cost_unbudgeted(): + goal = ALL.expressions['goal'] + assert ALL.objective.expression == 'goal', 'the objective is the goal' + assert goal.otherwise == 'system_cost', 'without the flag the goal is the system cost' + assert [case.when for case in goal.cases.values()] == ['mga'], 'only the mga flag switches the goal' + assert ALL.constraints['budget'].where == 'mga', 'without the flag there is no budget row' From 315151952e478121820108a1cb281791d10a02cd Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 14:15:53 +0200 Subject: [PATCH 17/46] docs(pypsa): mga leaves the standard spec until it can land as a patch file Reverts 51096860. The commit is kept on docs/pypsa-mga and waits for #571. --- docs/examples/pypsa.md | 184 +++------------------- examples/pypsa.yaml | 104 ++---------- examples/references/pypsa/reference.py | 13 +- examples/references/pypsa/references.json | 23 --- examples/references/pypsa/rung_31_mga.py | 30 ---- examples/symbols/pypsa.yaml | 9 -- tests/test_pypsa_collapses.py | 19 +-- 7 files changed, 46 insertions(+), 336 deletions(-) delete mode 100644 examples/references/pypsa/rung_31_mga.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 580bb1a5..5788a998 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -86,8 +86,8 @@ def build(): | [`Link-fix-p-upper`](#link-fix-p-upper) | done | | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | a loaded bus with nothing attached: PyPSA refuses, see X2 | | `Bus-meshed-*-nodal_balance` | out | the same balance rows, dealt into linopy containers by how many component columns name a bus — `meshed_thresholds`, an `n.optimize()` keyword defaulting to `[30, 100, 400]`. Same rows, same duals, another name; a modeler whose engine wants the split states it, the file does not (#123) | -| [`marginal_cost`](#system_cost) | done | | -| [`marginal_cost_quadratic`](#system_cost) | done | rung 10, below; Generator and Link — PyPSA also carries it on storage units and stores, one more term each of the same shape | +| [`marginal_cost`](#objective) | done | | +| [`marginal_cost_quadratic`](#objective) | done | rung 10, below; Generator and Link — PyPSA also carries it on storage units and stores, one more term each of the same shape | | `objective_constant` | split | an objective shift, compared net of `n._objective_constant` — rungs 11 and 13 carry a nonzero one, `21915277.52` and `160.0`, so the netting is under test | @@ -129,7 +129,7 @@ def build(): | [`StorageUnit-energy_balance`](#storageunit-energy_balance) | done | the charge carried into a snapshot is a cased quantity — cyclic, opening, carried; `(1-loss)**eh` is prep | | [`Store-energy_balance`](#store-energy_balance) | done | same | | [`StorageUnit-p_set`](#storageunit-p_set), [`{c}-{attr}_set`](#generator-p_set) | done | `Generator-p_set`, `Link-p_set`, `StorageUnit-state_of_charge_set`, `Store-e_set`, `Line-s_set` | -| [`marginal_cost_storage`, `spill_cost`](#system_cost) | done | | +| [`marginal_cost_storage`, `spill_cost`](#objective) | done | | > ✔ `pypsa 1.3.0` solves this rung's network at objective `4456.659315422356`, 103 rows. @@ -203,7 +203,7 @@ def build(): | [`{c}-ext-p_nom-lower/upper`](#generator-ext-p_nom-lower) | done | | | [`{c}-p_nom_set`](#generator-p_nom_set) | done | | | [`Generator-e_sum_min/max`](#generator-e_sum_min) | done | | -| [capital cost](#system_cost) | done | `periodized_cost` is an annuity, data prep | +| [capital cost](#objective) | done | `periodized_cost` is an annuity, data prep | > ✔ `pypsa 1.3.0` solves this rung's network at objective `7633.908502024292`, 184 rows. @@ -604,7 +604,7 @@ def build(): | [`{c}-com-up-time`, `-down-time`](#generator-com-up-time) | done | `sum_back(window=min_up_time)` | | [`{c}-com-status-min_up_time_must_stay_up`](#generator-com-status-min_up_time_must_stay_up) | done | the window is a prep mask — `position()` takes a literal | | [`{c}-com-status-min_down_time_must_stay_up`](#generator-com-status-min_down_time_must_stay_up) | done | the same prep mask over the down time brought in, status zero; PyPSA's name says `_must_stay_up`; rung 24 records it | -| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#system_cost) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | | [`{c}-com-p-before/-current/-partly-*`](pypsa_linearized_uc.md) | done | rung 12, a file of its own | @@ -823,7 +823,7 @@ feeds zero, so the term vanishes and the objective stays linear. | PyPSA | status | note | | --- | --- | --- | -| [`marginal_cost_quadratic`](#system_cost) | done | degree 2 in the objective; Generator and Link here | +| [`marginal_cost_quadratic`](#objective) | done | degree 2 in the objective; Generator and Link here | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12587.437500000098`, 60 rows. @@ -1304,7 +1304,7 @@ excess per scenario and the tail's average, blended into the objective at | [`CVaR-a`, `CVaR-theta`, `CVaR`](#variable-domains) | done | | | [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension | | [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep | -| [objective](#system_cost) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | +| [objective](#objective) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | > ✔ `pypsa 1.3.0` solves this rung's network at objective `9267.386666666665`, 87 rows. @@ -1357,7 +1357,7 @@ own dimensions. | [`Generator-p`](#variable-domains) | done | where the generator stands in the snapshot's period — `active`, data prep | | [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | | [`Carrier-growth_limit`](#carrier-growth_limit) | done | every extendable component of the carrier, counted in the first period a build stands in; `edge=0` at the first period | -| [objective](#system_cost) | done | period weight on operation; capacity once per period it stands in | +| [objective](#objective) | done | period weight on operation; capacity once per period it stands in | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | @@ -1548,7 +1548,7 @@ generator's and the link's, read over a converter. | [`Process-p_set`](#process-p_set) | done | a fixed internal power schedule | | [`Process-p_nom_set`](#process-p_nom_set) | done | a fixed built capacity | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | each port enters at its `rate` | -| [objective](#system_cost) | done | marginal cost on internal power; capital on capacity | +| [objective](#objective) | done | marginal cost on internal power; capital on capacity | > ✔ `pypsa 1.3.0` solves this rung's network at objective `9730.0`, 70 rows. @@ -1624,7 +1624,7 @@ transformer's built capacity. | [`Transformer-s_nom_set`](#transformer-s_nom_set) | done | a fixed built capacity | | [`Kirchhoff-Voltage-Law`](#kirchhoff-voltage-law) | done | rung 6, over `x_pu_eff` and a phase shift | | [`Transformer-phase_shift`](#variable-domains) | done | rung 20, an optimised phase shift | -| [objective](#system_cost) | done | capital on capacity | +| [objective](#objective) | done | capital on capacity | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12274.401472395122`, 106 rows. @@ -2047,7 +2047,7 @@ extendable, modular, or both. | [`Link-com-status-min_up_time_must_stay_up`, `-min_down_time_must_stay_up`](#link-com-status-min_up_time_must_stay_up) | done | prep masks, as the generator's | | [`Link-p-ramp_limit_*`, `-*-bigM`](#link-p-ramp_limit_up) | done | the generator's cased allowance and big-M rows over flow | | [`Link-p_nom_modularity`](#link-p_nom_modularity) | done | | -| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#system_cost) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | > ✔ `pypsa 1.3.0` solves this rung's network at objective `14013.0`, 235 rows. @@ -2169,7 +2169,7 @@ time rules bind. | [`Process-com-status-min_up_time_must_stay_up`, `-min_down_time_must_stay_up`](#process-com-status-min_up_time_must_stay_up) | done | prep masks, as the generator's | | [`Process-p-ramp_limit_*`, `-*-bigM`](#process-p-ramp_limit_up) | done | the generator's cased allowance and big-M rows over internal power | | [`Process-p_nom_modularity`](#process-p_nom_modularity) | done | | -| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#system_cost) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | > ✔ `pypsa 1.3.0` solves this rung's network at objective `15956.125`, 235 rows. @@ -2552,77 +2552,6 @@ def build(): -### Rung 31 — MGA - -`optimize_mga` needs a solved network. It builds the model again, adds one -`budget` row, and replaces the objective (`mga.py:343`, `:350`, `:372`). The row -caps the model's objective plus `installed_capex` at `(1 + slack)` times -`capex + opex` of the optimum (`mga.py:256`, `:270`). Net of the objective -constant, that is `system_cost <= mga_budget`, and data prep computes the -budget from the optimum. The slack also scales the capital cost of fixed -assets, which the objective does not carry. In this rung, the 45 of fixed -hydro capex adds 4.5 to the cap. The new objective is the weights times the -variables they name, times `-1` to maximise (`mga.py:187`, `:372`). -`optimize_mga_in_direction` builds the same row and minimises the negated sum -of direction times dimension weights (`mga.py:508`, `:513`). Data prep folds -the sense and the direction into `{c}_mga_weight`, so one `goal` case covers -both. - -This rung maximises the wind build at a slack of 0.1. The optimum costs -`9884.17` and builds 116.67 of wind. The MGA run builds 174.85, and the -`budget` row holds with equality at dual `-0.05`. - -| PyPSA | status | note | -| --- | --- | --- | -| [`budget`](#budget) | done | the `mga` flag builds the row; `mga_budget` is `(1 + slack) * optimal_cost - fixed_cost` plus the objective constant, data prep. The multi-period and stochastic budgets are PyPSA's `statistics` sums, data prep, and no rung records them | -| [MGA objective](#goal) | done | `goal` is the weighted builds where `mga` holds and `system_cost` elsewhere; a plain run is the standard objective | -| `weights` on a variable other than a build | open | PyPSA takes any `{c}-{attr}`, for example `Generator-p` per snapshot; each is one more parameter and one more term of the same shape | -| `optimize_mga_in_multiple_directions`, `project_solved` | out | a loop of `optimize_mga_in_direction` runs, and a read-back of their solutions | - - -> ✔ `pypsa 1.3.0` solves this rung's network at objective `-174.8541663000001`, 63 rows. - -
-The network, as PyPSA code - -`rung_31_mga.py` - -```python -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -"""Rung 31: MGA — the most wind a network builds within a tenth above its least cost.""" - -from __future__ import annotations - -import spine - -MGA = {'weights': {'Generator': {'p_nom': {'wind31': 1}}}, 'sense': 'max', 'slack': 0.1} - - -def build(): - """The spine plus a bus that extendable wind and gas and a fixed hydro unit with a capital cost serve; the wind carries a build already, so the objective has a constant.""" - n = spine.build() - n.add('Bus', 'mga31') - n.add( - 'Generator', - 'wind31', - bus='mga31', - p_nom=10, - p_nom_extendable=True, - capital_cost=20, - p_max_pu=[0.3, 0.9, 0.5, 0.6], - ) - n.add('Generator', 'gas31', bus='mga31', p_nom_extendable=True, capital_cost=8, marginal_cost=40) - n.add('Generator', 'hydro31', bus='mga31', p_nom=15, capital_cost=3, marginal_cost=5) - n.add('Load', 'mga31_load', bus='mga31', p_set=[50, 80, 60, 70]) - return n -``` - -
- - ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2647,7 +2576,7 @@ concatenation of the regime blocks, `p0`/`p1` derived from `Link-p`. ## The file -A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. Under the `mga` flag the file is PyPSA's `optimize_mga`: the system cost is capped at a budget above its optimum, and weighted builds are the objective instead. +A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. #### Sets @@ -2761,15 +2690,6 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | | $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor | -| $`\mathrm{mga}`$ | `mga` (scalar) — whether the run is PyPSA's `optimize_mga` or `optimize_mga_in_direction` — the system cost is capped by the `budget` row and the weighted builds are the objective; false is a plain `n.optimize()` | -| $`\overline{\mathrm{C}}`$ | `mga_budget` (scalar) — the most the system cost may reach under MGA — PyPSA's `(1 + slack) * optimal_cost - fixed_cost`, from the solved optimum's statistics, plus the objective constant this file leaves out of the cost; data prep | -| $`\beta`$ | `Generator_mga_weight` over $`\mathcal{G}`$ — what one unit of a generator's build weighs in the MGA objective — PyPSA's `weights['Generator']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | -| $`\beta^{f}`$ | `Link_mga_weight` over $`\mathcal{L}`$ — what one unit of a link's build weighs in the MGA objective — PyPSA's `weights['Link']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | -| $`\beta^{h}`$ | `StorageUnit_mga_weight` over $`\mathcal{S}`$ — what one unit of a storage unit's build weighs in the MGA objective — PyPSA's `weights['StorageUnit']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | -| $`\beta^{e}`$ | `Store_mga_weight` over $`\mathcal{V}`$ — what one unit of a store's build weighs in the MGA objective — PyPSA's `weights['Store']['e_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | -| $`\beta^{s}`$ | `Line_mga_weight` over $`\mathcal{K}`$ — what one unit of a line's build weighs in the MGA objective — PyPSA's `weights['Line']['s_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | -| $`\beta^{z}`$ | `Process_mga_weight` over $`\mathcal{J}`$ — what one unit of a process's build weighs in the MGA objective — PyPSA's `weights['Process']['p_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | -| $`\beta^{\sigma}`$ | `Transformer_mga_weight` over $`\mathcal{M}`$ — what one unit of a transformer's build weighs in the MGA objective — PyPSA's `weights['Transformer']['s_nom']`, negated to maximise; a direction's weighted dimensions fold into it, data prep | | $`\mathrm{w}^{y}`$ | `period_weight_objective` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.objective` — what a period's cost weighs | | $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep | | $`\mathrm{on}^{f}`$ | `Link_active` over $`\mathcal{T} \times \mathcal{L}`$ — whether a link stands in a snapshot's period — PyPSA's `active`, data prep | @@ -2988,8 +2908,6 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\check{s}`$ | `Line_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the flow a line's post-contingency rows read — its flow where it stands, nothing where it does not, since PyPSA builds those rows for every branch of the sub-network in every snapshot | | $`\check{\sigma}`$ | `Transformer_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — the flow a transformer's post-contingency rows read, as a line's | | $`\hat{s}`$ | `Outage_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}^{\mathrm{out}}`$ — the flow an outage takes off its branch — the outaged line's or transformer's flow before it goes out | -| $`\mathit{system\_cost}`$ | `system_cost` (scalar) — what `n.optimize()` minimises — capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail | -| $`\mathit{goal}`$ | `goal` (scalar) — what the run minimises — the system cost, or under MGA the weighted builds within the budget | Upright is what the model is given — a parameter such as $`\mathrm{Transformer\_phase\_shift\_varying}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{Transformer\_phase\_shift}`$. An index is italic too, being what a quantifier chooses, and a set is script. @@ -3008,12 +2926,21 @@ $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ counts within the group a relation pu ```yaml objective: sense: minimize - description: the system cost; under MGA the weighted builds instead - expression: goal + description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail + expression: >- + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + CVaR_omega * CVaR ``` ```math -\min \mathit{goal} +\min \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{c}^{\mathrm{cap},z}_{j} \cdot \mathrm{W}^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \mathrm{c}^{\mathrm{cap},\sigma}_{m} \cdot \mathrm{W}^{\sigma}_{m} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR ``` ### `Generator-fix-p-lower` @@ -6342,22 +6269,6 @@ CVaR_def: \theta + \mathrm{v} \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot a_{\xi} \right) \le CVaR ``` -### `budget` - -`budget` - -```yaml -budget: - description: "`budget` — under MGA, the system cost stays within the slack above its optimum" - dims: [] - where: mga - expression: system_cost <= mga_budget -``` - -```math -\mathit{system\_cost} \le \overline{\mathrm{C}} \qquad \text{where } \mathrm{mga} -``` - ### `Generator_previous_status` ```yaml @@ -7200,51 +7111,6 @@ Outage_s: \hat{s}_{\xi,t,\kappa} = \begin{cases} \check{s}_{\xi,t,\mathrm{Outage\_line}(\kappa)} & \text{if } \mathrm{Outage\_line}(\kappa) \text{ is defined} \\ \check{\sigma}_{\xi,t,\mathrm{Outage\_transformer}(\kappa)} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ \kappa \in \mathcal{K}^{\mathrm{out}} ``` -### `system_cost` - -```yaml -system_cost: - description: what `n.optimize()` minimises — capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail - expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) - + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) - + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) - + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) - + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) - + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) - + CVaR_omega * CVaR -``` - -```math -\mathit{system\_cost} = \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{c}^{\mathrm{cap},z}_{j} \cdot \mathrm{W}^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \mathrm{c}^{\mathrm{cap},\sigma}_{m} \cdot \mathrm{W}^{\sigma}_{m} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR -``` - -### `goal` - -```yaml -goal: - description: what the run minimises — the system cost, or under MGA the weighted builds within the budget - dims: [] - cases: - mga: - when: mga - expression: >- - sum(Generator_p_nom_ext * Generator_mga_weight) - + sum(Link_p_nom_ext * Link_mga_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_mga_weight) - + sum(Store_e_nom_ext * Store_mga_weight) - + sum(Line_s_nom_ext * Line_mga_weight) - + sum(Process_p_nom_ext * Process_mga_weight) - + sum(Transformer_s_nom_ext * Transformer_mga_weight) - otherwise: system_cost -``` - -```math -\mathit{goal} = \begin{cases} \sum_{g \in \mathcal{G}} P_{g} \cdot \beta_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \beta^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \beta^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \beta^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \beta^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \beta^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \beta^{\sigma}_{m} & \text{if } \mathrm{mga} \\ \mathit{system\_cost} & \text{otherwise} \end{cases} -``` - #### Variable domains **`Generator_p`** diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 3d853972..9b0d747b 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -13,9 +13,7 @@ description: >- model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the - outage factors are data prep. Under the `mga` flag the file is PyPSA's - `optimize_mga`: the system cost is capped at a budget above its optimum, and - weighted builds are the objective instead. + outage factors are data prep. dimensions: scenario: @@ -409,61 +407,6 @@ parameters: CVaR_inv_tail: description: PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor dims: [] - mga: - description: >- - whether the run is PyPSA's `optimize_mga` or `optimize_mga_in_direction` - — the system cost is capped by the `budget` row and the weighted builds - are the objective; false is a plain `n.optimize()` - dims: [] - dtype: bool - mga_budget: - description: >- - the most the system cost may reach under MGA — PyPSA's `(1 + slack) * - optimal_cost - fixed_cost`, from the solved optimum's statistics, plus - the objective constant this file leaves out of the cost; data prep - dims: [] - Generator_mga_weight: - description: >- - what one unit of a generator's build weighs in the MGA objective — PyPSA's - `weights['Generator']['p_nom']`, negated to maximise; a direction's weighted - dimensions fold into it, data prep - dims: [generator] - Link_mga_weight: - description: >- - what one unit of a link's build weighs in the MGA objective — PyPSA's - `weights['Link']['p_nom']`, negated to maximise; a direction's weighted - dimensions fold into it, data prep - dims: [link] - StorageUnit_mga_weight: - description: >- - what one unit of a storage unit's build weighs in the MGA objective — PyPSA's - `weights['StorageUnit']['p_nom']`, negated to maximise; a direction's weighted - dimensions fold into it, data prep - dims: [storage_unit] - Store_mga_weight: - description: >- - what one unit of a store's build weighs in the MGA objective — PyPSA's - `weights['Store']['e_nom']`, negated to maximise; a direction's weighted - dimensions fold into it, data prep - dims: [store] - Line_mga_weight: - description: >- - what one unit of a line's build weighs in the MGA objective — PyPSA's - `weights['Line']['s_nom']`, negated to maximise; a direction's weighted - dimensions fold into it, data prep - dims: [line] - Process_mga_weight: - description: >- - what one unit of a process's build weighs in the MGA objective — PyPSA's - `weights['Process']['p_nom']`, negated to maximise; a direction's weighted - dimensions fold into it, data prep - dims: [process] - Transformer_mga_weight: - description: >- - what one unit of a transformer's build weighs in the MGA objective — PyPSA's - `weights['Transformer']['s_nom']`, negated to maximise; a direction's weighted - dimensions fold into it, data prep - dims: [transformer] period_weight_objective: description: PyPSA's `investment_period_weightings.objective` — what a period's cost weighs dims: [period] @@ -1731,33 +1674,6 @@ expressions: cases: line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line, over=line, into=outage)" } otherwise: at(Transformer_s_monitored, by=Outage_transformer, over=transformer, into=outage) - system_cost: - description: what `n.optimize()` minimises — capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail - expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) - + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) - + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) - + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) - + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) - + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) - + CVaR_omega * CVaR - goal: - description: what the run minimises — the system cost, or under MGA the weighted builds within the budget - dims: [] - cases: - mga: - when: mga - expression: >- - sum(Generator_p_nom_ext * Generator_mga_weight) - + sum(Link_p_nom_ext * Link_mga_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_mga_weight) - + sum(Store_e_nom_ext * Store_mga_weight) - + sum(Line_s_nom_ext * Line_mga_weight) - + sum(Process_p_nom_ext * Process_mga_weight) - + sum(Transformer_s_nom_ext * Transformer_mga_weight) - otherwise: system_cost constraints: Generator_fix_p_lower: @@ -3062,13 +2978,17 @@ constraints: description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" dims: [] expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR - budget: - description: "`budget` — under MGA, the system cost stays within the slack above its optimum" - dims: [] - where: mga - expression: system_cost <= mga_budget objective: sense: minimize - description: the system cost; under MGA the weighted builds instead - expression: goal + description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail + expression: >- + sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + CVaR_omega * CVaR diff --git a/examples/references/pypsa/reference.py b/examples/references/pypsa/reference.py index b8df1f91..0c70a779 100644 --- a/examples/references/pypsa/reference.py +++ b/examples/references/pypsa/reference.py @@ -17,9 +17,7 @@ the PyPSA model under review is the script itself. A rung stated by a file of its own names it as `MODEL`; one that needs `n.optimize` keywords names them as `OPTIMIZE`; one that names `BRANCH_OUTAGES` is solved by -`n.optimize.optimize_security_constrained` over them. A rung that names `MGA` -is solved once more through `n.optimize.optimize_mga` with those keywords, and -that solve is the record. PyPSA is not a dependency +`n.optimize.optimize_security_constrained` over them. PyPSA is not a dependency of this project; this script pins the versions the recorded numbers are from, and the `PyPSA references` workflow runs `--check` on every change. """ @@ -48,9 +46,9 @@ def build(rung: str) -> pypsa.Network: return importlib.import_module(rung).build() -def keywords(rung: str, call: str = 'OPTIMIZE') -> dict[str, object]: - """What the rung's `n.optimize` (or `optimize_mga`, for `MGA`) takes beyond the solver — the script's `call`, if it names one.""" - return dict(getattr(importlib.import_module(rung), call, {})) +def keywords(rung: str) -> dict[str, object]: + """What the rung's `n.optimize` takes beyond the solver — the script's `OPTIMIZE`, if it names any.""" + return dict(getattr(importlib.import_module(rung), 'OPTIMIZE', {})) def record(n: pypsa.Network) -> dict[str, object]: @@ -78,9 +76,6 @@ def solved(rung: str) -> dict[str, object]: solver_name='highs', branch_outages=outages, **keywords(rung) ) assert status == 'ok', f'{rung}: HiGHS did not solve — {status} / {condition}' - if mga := keywords(rung, 'MGA'): - status, condition = n.optimize.optimize_mga(solver_name='highs', **mga) - assert status == 'ok', f'{rung}: HiGHS did not solve the MGA run — {status} / {condition}' return record(n) diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 398e7161..62698829 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1155,28 +1155,5 @@ "Transformer-fix-s-upper-security-for-Line-outage-in-sub-network-2": 8, "Transformer-fix-s-upper-security-for-Transformer-outage-in-sub-network-2": 4 } - }, - "rung_31_mga": { - "columns": { - "Generator-p": 20, - "Generator-p_nom": 2, - "Link-p": 4, - "objective_constant": 1 - }, - "global_constraints": {}, - "objective": -174.8541663000001, - "objective_constant": 200.0, - "pypsa": "1.3.0", - "rows": { - "Bus-nodal_balance": 12, - "Generator-ext-p-lower": 8, - "Generator-ext-p-upper": 8, - "Generator-ext-p_nom-lower": 2, - "Generator-fix-p-lower": 12, - "Generator-fix-p-upper": 12, - "Link-fix-p-lower": 4, - "Link-fix-p-upper": 4, - "budget": 1 - } } } diff --git a/examples/references/pypsa/rung_31_mga.py b/examples/references/pypsa/rung_31_mga.py deleted file mode 100644 index 55bd8522..00000000 --- a/examples/references/pypsa/rung_31_mga.py +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: math-spec Contributors -# -# SPDX-License-Identifier: MIT - -"""Rung 31: MGA — the most wind a network builds within a tenth above its least cost.""" - -from __future__ import annotations - -import spine - -MGA = {'weights': {'Generator': {'p_nom': {'wind31': 1}}}, 'sense': 'max', 'slack': 0.1} - - -def build(): - """The spine plus a bus that extendable wind and gas and a fixed hydro unit with a capital cost serve; the wind carries a build already, so the objective has a constant.""" - n = spine.build() - n.add('Bus', 'mga31') - n.add( - 'Generator', - 'wind31', - bus='mga31', - p_nom=10, - p_nom_extendable=True, - capital_cost=20, - p_max_pu=[0.3, 0.9, 0.5, 0.6], - ) - n.add('Generator', 'gas31', bus='mga31', p_nom_extendable=True, capital_cost=8, marginal_cost=40) - n.add('Generator', 'hydro31', bus='mga31', p_nom=15, capital_cost=3, marginal_cost=5) - n.add('Load', 'mga31_load', bus='mga31', p_set=[50, 80, 60, 70]) - return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 790e06c9..2f2eb235 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -295,15 +295,6 @@ names: Line_capital_weight: '\mathrm{W}^{s}' Process_capital_weight: '\mathrm{W}^{z}' Transformer_capital_weight: '\mathrm{W}^{\sigma}' - mga: '\mathrm{mga}' - mga_budget: '\overline{\mathrm{C}}' - Generator_mga_weight: '\beta' - Link_mga_weight: '\beta^{f}' - StorageUnit_mga_weight: '\beta^{h}' - Store_mga_weight: '\beta^{e}' - Line_mga_weight: '\beta^{s}' - Process_mga_weight: '\beta^{z}' - Transformer_mga_weight: '\beta^{\sigma}' Generator_first_active: '\mathrm{new}' Link_first_active: '\mathrm{new}^{f}' StorageUnit_first_active: '\mathrm{new}^{h}' diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index 2350e557..344a8561 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -5,9 +5,8 @@ """`examples/pypsa.yaml` is the superset that collapses to the standard PyPSA model. The file always declares a `scenario` axis, a mask-only `period` axis, a `carrier` -axis, the CVaR rows and the MGA `budget` row. Fed one scenario, one period, -all-active masks, unit weights and a false `mga` flag, every addition is a no-op -and the standard model returns. The repository +axis and the CVaR rows. Fed one scenario, one period, all-active masks and unit +weights, every addition is a no-op and the standard model returns. The repository runs no solver, so the reduction is guarded structurally: `tests/fixtures/ pypsa_standard_shape.yaml` freezes the standard model's names and frames, and the only frame change a standard row may carry is a leading `scenario`. @@ -94,16 +93,8 @@ def test_the_extra_axes_and_rows_are_declared(): assert 'scenario_opex' in ALL.expressions, 'the per-scenario operating cost is a named expression' -def test_omega_blends_expectation_and_tail_in_the_system_cost(): - expr = ALL.expressions['system_cost'].expression +def test_omega_blends_expectation_and_tail_in_the_objective(): + expr = ALL.objective.expression assert all(term in expr for term in ('CVaR_omega', 'scenario_weight', 'scenario_opex', 'CVaR')), ( - 'the system cost prices expected opex by scenario weight and blends the tail by omega' + 'the objective prices expected opex by scenario weight and blends the tail by omega' ) - - -def test_a_run_without_the_mga_flag_minimises_the_system_cost_unbudgeted(): - goal = ALL.expressions['goal'] - assert ALL.objective.expression == 'goal', 'the objective is the goal' - assert goal.otherwise == 'system_cost', 'without the flag the goal is the system cost' - assert [case.when for case in goal.cases.values()] == ['mga'], 'only the mga flag switches the goal' - assert ALL.constraints['budget'].where == 'mga', 'without the flag there is no budget row' From 3f5192a32ebc92fc7599ae97b90f45fc7c47100e Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 23 Sep 2026 15:24:24 +0200 Subject: [PATCH 18/46] docs(pypsa): a storage built in a later period opens at its first active snapshot --- docs/examples/pypsa.md | 146 +++++++++++++++--- examples/pypsa.yaml | 76 +++++++-- examples/references/pypsa/references.json | 29 ++++ .../pypsa/rung_32_storage_later_period.py | 46 ++++++ examples/symbols/pypsa.yaml | 4 + 5 files changed, 265 insertions(+), 36 deletions(-) create mode 100644 examples/references/pypsa/rung_32_storage_later_period.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 5788a998..b5b6a848 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1359,6 +1359,7 @@ own dimensions. | [`Carrier-growth_limit`](#carrier-growth_limit) | done | every extendable component of the carrier, counted in the first period a build stands in; `edge=0` at the first period | | [objective](#objective) | done | period weight on operation; capacity once per period it stands in | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | +| [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) for storage built in a later period or retired early | done | rung 32 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12747.19109626398`, 80 rows. @@ -2552,6 +2553,91 @@ def build(): +### Rung 32 — storage that stands in one period only + +`n.optimize(multi_investment_periods=True)` with storage that is not per period +and does not stand in every period. PyPSA opens such a storage at the first +snapshot it stands in: on its initial level where it is not cyclic, and on the +level of the last snapshot it stands in where it is cyclic +(`constraints.py:2095-2097`, `2270-2273`). It reads the previous level through +a forward fill over the snapshots the storage does not stand in. Because a +build year and a lifetime make those snapshots one run at each end of the +horizon, the file states the same rows with two data-prep parameters. The +opening snapshot past the horizon's first is `{c}_opens_late`. The number of +snapshots the storage does not stand in is `{c}_inactive_snapshots`, and a +cyclic storage reaches back that many snapshots further. A plain run feeds +false and zero, so the rows collapse to the standard ones. + +The rung builds a cyclic storage unit and a store with an initial level of 5 in +2030, and a cyclic store that retires after 2020. The earlier file read the +level before 2030 as absent and dropped the opening row, so each storage +opened on any level it chose. With the same rows, the objective falls to +`5620.61` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance), at the first snapshot a storage stands in | done | the `cyclic` and `opening` cases hold at `position(snapshot) == 0` or at `{c}_opens_late`; the cyclic one shifts one snapshot and then `{c}_inactive_snapshots` more, `edge='wrap'` | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `7230.4866975671375`, 92 rows. + +
+The network, as PyPSA code + +`rung_32_storage_later_period.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 32: storage that stands in one period only — two built in the later period open at its first snapshot, and a cyclic one that retires closes on its own last snapshot.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: eight snapshots over two periods, two storages built in 2030, one cyclic store that retires after 2020.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 2.0, 1.5, 2.5, 2.0] + n.snapshot_weightings['stores'] = [0.5, 2.0, 1.5, 2.5, 0.5, 2.0, 1.5, 2.5] + n.add('Bus', 'hub') + n.add('Generator', 'base32', bus='hub', p_nom=100, marginal_cost=10) + n.add('Generator', 'peak32', bus='hub', p_nom=200, marginal_cost=[80, 20, 90, 30, 80, 20, 90, 30]) + n.add( + 'StorageUnit', + 'su_late', + bus='hub', + p_nom=15, + max_hours=4, + standing_loss=0.02, + cyclic_state_of_charge=True, + build_year=2030, + lifetime=30, + ) + n.add('Store', 'e_late', bus='hub', e_nom=30, e_initial=5, build_year=2030, lifetime=30) + n.add('Store', 'e_retire', bus='hub', e_nom=30, standing_loss=0.01, e_cyclic=True, build_year=2020, lifetime=10) + n.add('Load', 'hub_load', bus='hub', p_set=[40, 60, 70, 40, 90, 110, 120, 90]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2749,6 +2835,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{cyc}`$ | `StorageUnit_cyclic_state_of_charge` over $`\mathcal{S}`$ — whether the horizon closes on itself instead of opening on the initial charge | | $`\mathrm{cyc}^{y}`$ | `StorageUnit_cyclic_state_of_charge_per_period` over $`\mathcal{S}`$ — whether each investment period closes on itself instead of carrying its charge on to the next; it overrides `cyclic_state_of_charge` and `state_of_charge_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{reset}`$ | `StorageUnit_state_of_charge_initial_per_period` over $`\mathcal{S}`$ — whether each investment period opens on the initial charge instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\mathrm{open}`$ | `StorageUnit_opens_late` over $`\mathcal{T} \times \mathcal{S}`$ — whether a snapshot is the first a storage unit stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every unit stands throughout | +| $`\mathrm{idle}`$ | `StorageUnit_inactive_snapshots` over $`\mathcal{S}`$ — how many snapshots a storage unit does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic unit reaches back this many snapshots further, so it closes on the last snapshot it stands in | | $`\mathrm{c}^{h}`$ | `StorageUnit_marginal_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of dispatch | | $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | | $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | @@ -2763,6 +2851,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{cyc}^{e}`$ | `Store_e_cyclic` over $`\mathcal{V}`$ — whether the horizon closes on itself instead of opening on the initial energy | | $`\mathrm{cyc}^{e,y}`$ | `Store_e_cyclic_per_period` over $`\mathcal{V}`$ — whether each investment period closes on itself instead of carrying its energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{reset}^{e}`$ | `Store_e_initial_per_period` over $`\mathcal{V}`$ — whether each investment period opens on the initial energy instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\mathrm{open}^{e}`$ | `Store_opens_late` over $`\mathcal{T} \times \mathcal{V}`$ — whether a snapshot is the first a store stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every store stands throughout | +| $`\mathrm{idle}^{e}`$ | `Store_inactive_snapshots` over $`\mathcal{V}`$ — how many snapshots a store does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic store reaches back this many snapshots further, so it closes on the last snapshot it stands in | | $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | | $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | | $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | @@ -2894,8 +2984,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\widehat{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_committed` over $`\mathcal{J}`$ — the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{z,+}`$ | `Process_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may raise internal power between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{z,-}`$ | `Process_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may lower internal power between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | -| $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial charge at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise. Per period, the same holds with each investment period as the horizon | -| $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — its last snapshot's less standing loss where it is cyclic, the given initial energy at the start of the horizon, which no standing loss has touched yet, and the previous snapshot's less standing loss otherwise. Per period, the same holds with each investment period as the horizon | +| $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial charge, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A unit built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon | +| $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial energy, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A store built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon | | $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | | $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed transfer wraps from the horizon's end, and where it is not the energy still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | | $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{I}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | @@ -6817,20 +6907,28 @@ Process_ramp_down_allowance: ```yaml StorageUnit_charge_carried_in: description: >- - the charge a unit opens a snapshot with — its last snapshot's less - standing loss where it is cyclic, the given initial charge at the start - of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise. Per period, the same holds with - each investment period as the horizon + the charge a unit opens a snapshot with — at the first snapshot it + stands in, its last such snapshot's less standing loss where it is + cyclic and the given initial charge, which no standing loss has touched + yet, where it is not; the previous snapshot's less standing loss + otherwise. A unit built in a later period opens in that period, and a + cyclic one that retires closes on its own last snapshot. Per period, the + same holds with each investment period as the horizon dims: [scenario, snapshot, storage_unit] cases: cyclic: - when: StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period AND NOT StorageUnit_state_of_charge_initial_per_period - expression: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap') + when: >- + StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period + AND NOT StorageUnit_state_of_charge_initial_per_period + AND (position(snapshot) == 0 OR StorageUnit_opens_late) + expression: >- + StorageUnit_retention + * shift(shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap'), along=snapshot, offset=StorageUnit_inactive_snapshots, edge='wrap') opening: when: >- NOT StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period - AND NOT StorageUnit_state_of_charge_initial_per_period AND position(snapshot) == 0 + AND NOT StorageUnit_state_of_charge_initial_per_period + AND (position(snapshot) == 0 OR StorageUnit_opens_late) expression: StorageUnit_state_of_charge_initial period_cyclic: when: StorageUnit_cyclic_state_of_charge_per_period @@ -6846,7 +6944,7 @@ StorageUnit_charge_carried_in: ``` ```math -\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus 1,s} & \text{if } \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \neg \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \wedge \mathrm{pos}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \mathrm{reset}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} +\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{t,s} \cdot \mathit{soc}_{\xi,\left( t \ominus \mathrm{idle} \right) \ominus 1,s} & \text{if } \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \mathrm{soc}^{0}_{s} & \text{if } \neg \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \mathrm{reset}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} ``` ### `Store_energy_carried_in` @@ -6854,18 +6952,26 @@ StorageUnit_charge_carried_in: ```yaml Store_energy_carried_in: description: >- - the energy a store opens a snapshot with — its last snapshot's less - standing loss where it is cyclic, the given initial energy at the start - of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise. Per period, the same holds with - each investment period as the horizon + the energy a store opens a snapshot with — at the first snapshot it + stands in, its last such snapshot's less standing loss where it is + cyclic and the given initial energy, which no standing loss has touched + yet, where it is not; the previous snapshot's less standing loss + otherwise. A store built in a later period opens in that period, and a + cyclic one that retires closes on its own last snapshot. Per period, the + same holds with each investment period as the horizon dims: [scenario, snapshot, store] cases: cyclic: - when: Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period - expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap') + when: >- + Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period + AND (position(snapshot) == 0 OR Store_opens_late) + expression: >- + Store_retention + * shift(shift(Store_e, along=snapshot, offset=1, edge='wrap'), along=snapshot, offset=Store_inactive_snapshots, edge='wrap') opening: - when: NOT Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period AND position(snapshot) == 0 + when: >- + NOT Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period + AND (position(snapshot) == 0 OR Store_opens_late) expression: Store_e_initial period_cyclic: when: Store_e_cyclic_per_period @@ -6877,7 +6983,7 @@ Store_energy_carried_in: ``` ```math -\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{t,v} \cdot e_{\xi,t \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \neg \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \mathrm{pos}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \mathrm{reset}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} +\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{v} & \text{if } \neg \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \mathrm{reset}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} ``` ### `Link_output_arrival` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 9b0d747b..ca421792 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -602,6 +602,20 @@ parameters: `multi_investment_periods`, so data prep feeds false otherwise dims: [storage_unit] dtype: bool + StorageUnit_opens_late: + description: >- + whether a snapshot is the first a storage unit stands in, where that is + not the first of the horizon — PyPSA's `active.cumsum() == 1` past the + first snapshot, data prep; false in a run where every unit stands throughout + dims: [snapshot, storage_unit] + dtype: bool + StorageUnit_inactive_snapshots: + description: >- + how many snapshots a storage unit does not stand in — PyPSA's + `(~active).sum()`, data prep. A cyclic unit reaches back this many + snapshots further, so it closes on the last snapshot it stands in + dims: [storage_unit] + dtype: int StorageUnit_marginal_cost: description: cost of one unit of dispatch dims: [snapshot, storage_unit] @@ -655,6 +669,20 @@ parameters: `multi_investment_periods`, so data prep feeds false otherwise dims: [store] dtype: bool + Store_opens_late: + description: >- + whether a snapshot is the first a store stands in, where that is not the + first of the horizon — PyPSA's `active.cumsum() == 1` past the first + snapshot, data prep; false in a run where every store stands throughout + dims: [snapshot, store] + dtype: bool + Store_inactive_snapshots: + description: >- + how many snapshots a store does not stand in — PyPSA's `(~active).sum()`, + data prep. A cyclic store reaches back this many snapshots further, so it + closes on the last snapshot it stands in + dims: [store] + dtype: int Store_marginal_cost: description: cost of one unit of power delivered dims: [snapshot, store] @@ -1503,20 +1531,28 @@ expressions: otherwise: Process_ramp_down_rate * Process_p_nom_effective StorageUnit_charge_carried_in: description: >- - the charge a unit opens a snapshot with — its last snapshot's less - standing loss where it is cyclic, the given initial charge at the start - of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise. Per period, the same holds with - each investment period as the horizon + the charge a unit opens a snapshot with — at the first snapshot it + stands in, its last such snapshot's less standing loss where it is + cyclic and the given initial charge, which no standing loss has touched + yet, where it is not; the previous snapshot's less standing loss + otherwise. A unit built in a later period opens in that period, and a + cyclic one that retires closes on its own last snapshot. Per period, the + same holds with each investment period as the horizon dims: [scenario, snapshot, storage_unit] cases: cyclic: - when: StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period AND NOT StorageUnit_state_of_charge_initial_per_period - expression: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap') + when: >- + StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period + AND NOT StorageUnit_state_of_charge_initial_per_period + AND (position(snapshot) == 0 OR StorageUnit_opens_late) + expression: >- + StorageUnit_retention + * shift(shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap'), along=snapshot, offset=StorageUnit_inactive_snapshots, edge='wrap') opening: when: >- NOT StorageUnit_cyclic_state_of_charge AND NOT StorageUnit_cyclic_state_of_charge_per_period - AND NOT StorageUnit_state_of_charge_initial_per_period AND position(snapshot) == 0 + AND NOT StorageUnit_state_of_charge_initial_per_period + AND (position(snapshot) == 0 OR StorageUnit_opens_late) expression: StorageUnit_state_of_charge_initial period_cyclic: when: StorageUnit_cyclic_state_of_charge_per_period @@ -1531,18 +1567,26 @@ expressions: otherwise: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1) Store_energy_carried_in: description: >- - the energy a store opens a snapshot with — its last snapshot's less - standing loss where it is cyclic, the given initial energy at the start - of the horizon, which no standing loss has touched yet, and the previous - snapshot's less standing loss otherwise. Per period, the same holds with - each investment period as the horizon + the energy a store opens a snapshot with — at the first snapshot it + stands in, its last such snapshot's less standing loss where it is + cyclic and the given initial energy, which no standing loss has touched + yet, where it is not; the previous snapshot's less standing loss + otherwise. A store built in a later period opens in that period, and a + cyclic one that retires closes on its own last snapshot. Per period, the + same holds with each investment period as the horizon dims: [scenario, snapshot, store] cases: cyclic: - when: Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period - expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap') + when: >- + Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period + AND (position(snapshot) == 0 OR Store_opens_late) + expression: >- + Store_retention + * shift(shift(Store_e, along=snapshot, offset=1, edge='wrap'), along=snapshot, offset=Store_inactive_snapshots, edge='wrap') opening: - when: NOT Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period AND position(snapshot) == 0 + when: >- + NOT Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period + AND (position(snapshot) == 0 OR Store_opens_late) expression: Store_e_initial period_cyclic: when: Store_e_cyclic_per_period diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 62698829..e2db48d7 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1155,5 +1155,34 @@ "Transformer-fix-s-upper-security-for-Line-outage-in-sub-network-2": 8, "Transformer-fix-s-upper-security-for-Transformer-outage-in-sub-network-2": 4 } + }, + "rung_32_storage_later_period": { + "columns": { + "Generator-p": 16, + "StorageUnit-p_dispatch": 4, + "StorageUnit-p_store": 4, + "StorageUnit-state_of_charge": 4, + "Store-e": 8, + "Store-p": 8 + }, + "global_constraints": {}, + "objective": 7230.4866975671375, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "StorageUnit-energy_balance": 4, + "StorageUnit-fix-p_dispatch-lower": 4, + "StorageUnit-fix-p_dispatch-upper": 4, + "StorageUnit-fix-p_store-lower": 4, + "StorageUnit-fix-p_store-upper": 4, + "StorageUnit-fix-state_of_charge-lower": 4, + "StorageUnit-fix-state_of_charge-upper": 4, + "Store-energy_balance": 8, + "Store-fix-e-lower": 8, + "Store-fix-e-upper": 8 + } } } diff --git a/examples/references/pypsa/rung_32_storage_later_period.py b/examples/references/pypsa/rung_32_storage_later_period.py new file mode 100644 index 00000000..1d6ec685 --- /dev/null +++ b/examples/references/pypsa/rung_32_storage_later_period.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 32: storage that stands in one period only — two built in the later period open at its first snapshot, and a cyclic one that retires closes on its own last snapshot.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: eight snapshots over two periods, two storages built in 2030, one cyclic store that retires after 2020.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 2.0, 1.5, 2.5, 2.0] + n.snapshot_weightings['stores'] = [0.5, 2.0, 1.5, 2.5, 0.5, 2.0, 1.5, 2.5] + n.add('Bus', 'hub') + n.add('Generator', 'base32', bus='hub', p_nom=100, marginal_cost=10) + n.add('Generator', 'peak32', bus='hub', p_nom=200, marginal_cost=[80, 20, 90, 30, 80, 20, 90, 30]) + n.add( + 'StorageUnit', + 'su_late', + bus='hub', + p_nom=15, + max_hours=4, + standing_loss=0.02, + cyclic_state_of_charge=True, + build_year=2030, + lifetime=30, + ) + n.add('Store', 'e_late', bus='hub', e_nom=30, e_initial=5, build_year=2030, lifetime=30) + n.add('Store', 'e_retire', bus='hub', e_nom=30, standing_loss=0.01, e_cyclic=True, build_year=2020, lifetime=10) + n.add('Load', 'hub_load', bus='hub', p_set=[40, 60, 70, 40, 90, 110, 120, 90]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 2f2eb235..d7ecaa05 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -254,6 +254,8 @@ names: StorageUnit_cyclic_state_of_charge: '\mathrm{cyc}' StorageUnit_cyclic_state_of_charge_per_period: '\mathrm{cyc}^{y}' StorageUnit_state_of_charge_initial_per_period: '\mathrm{reset}' + StorageUnit_opens_late: '\mathrm{open}' + StorageUnit_inactive_snapshots: '\mathrm{idle}' StorageUnit_marginal_cost: '\mathrm{c}^{h}' StorageUnit_marginal_cost_storage: '\mathrm{c}^{\mathrm{soc}}' StorageUnit_spill_cost: '\mathrm{c}^{\mathrm{spill}}' @@ -271,6 +273,8 @@ names: Store_e_cyclic: '\mathrm{cyc}^{e}' Store_e_cyclic_per_period: '\mathrm{cyc}^{e,y}' Store_e_initial_per_period: '\mathrm{reset}^{e}' + Store_opens_late: '\mathrm{open}^{e}' + Store_inactive_snapshots: '\mathrm{idle}^{e}' Store_marginal_cost: '\mathrm{c}^{q}' Store_marginal_cost_storage: '\mathrm{c}^{e}' Store_e_set: '\mathrm{e}^{\mathrm{set}}' From f0e167bcbd991a5bb569357b3154815032be351e Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 10:30:46 +0200 Subject: [PATCH 19/46] docs(pypsa): a unit may be scheduled off for maintenance --- docs/examples/pypsa.md | 1311 +++++++++++++++-- examples/pypsa.yaml | 471 +++++- examples/references/pypsa/references.json | 209 +++ .../references/pypsa/rung_33_maintenance.py | 82 ++ .../pypsa/rung_34_committable_maintenance.py | 99 ++ examples/symbols/pypsa.yaml | 24 + 6 files changed, 2015 insertions(+), 181 deletions(-) create mode 100644 examples/references/pypsa/rung_33_maintenance.py create mode 100644 examples/references/pypsa/rung_34_committable_maintenance.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index b5b6a848..9988d174 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -2638,6 +2638,272 @@ def build(): +### Rung 33 — maintenance + +A `Generator`, `Link` or `Process` with `maintainable=True` is taken off for +`maintenance_events` events (default 1) within the horizon. Each event covers +the snapshot it starts in and the snapshots after it, until their +`generators` weightings reach `maintenance_duration` hours +(`constraints.py:767-800`). While it is in maintenance, the component loses the +share `maintenance_pu` (default 1) of its build from both of its bounds +(`constraints.py:135-145`, `221-234`). The status `maintenance` is continuous in +[0, 1], and the binary starts make it whole (`variables.py:202-259`). An +extendable build multiplies a variable by a variable. PyPSA writes that product +as `maintenance_capacity` and holds it with four McCormick rows against +`p_nom_min` and `p_nom_max` (`constraints.py:810-845`), so PyPSA refuses an +extendable maintainable build with an infinite `p_nom_max`. + +The window of an event depends on the weightings, so its width varies along +`snapshot`. `sum_back` takes a width that does not vary along the dimension it +sums. So the file reads the windows as data: the relation +`*_maintenance_cover` pairs each start with the snapshots it covers. The mask +`*_maintenance_start_blocked` marks the starts whose window runs past the end +of the horizon or into a snapshot the component does not stand in. Both come +from `maintenance_duration`, the weightings and `active`. With them, each row +is the one PyPSA builds. Across investment periods an event may cross a period +boundary, and the count is over the whole horizon, as in PyPSA. Over scenarios +every maintenance variable is second stage, one schedule per scenario. + +Here every maintainable component is new, and a peaker at 60 covers the east +bus. With the same network and no `maintainable`, PyPSA solves to +`5388.833333333333`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Generator-maintenance`, `-maintenance_start`, `-maintenance_capacity`](#variable-domains), and the `Link` and `Process` ones | done | zero where the component is not maintainable | +| [`Generator-maint-event-count`](#generator-maint-event-count), [`-maint-window`](#generator-maint-window), [`-maint-start-horizon`](#generator-maint-start-horizon), and the `Link` and `Process` ones | done | the window and the blocked starts are data prep | +| [`Generator-maintcap_*`](#generator-maintcap_upper), and the `Link` and `Process` ones | done | `-maintcap_lower_nommin` only where `p_nom_min > 0` | +| [`Generator-fix-p-*`](#generator-fix-p-lower), [`-ext-p-*`](#generator-ext-p-lower), and the `Link` and `Process` ones, in maintenance | done | the bound less `maintenance_pu` of the build | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `7367.560185185185`, 179 rows. + +
+The network, as PyPSA code + +`rung_33_maintenance.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 33: maintenance — a fixed and an extendable generator, link and process, each taken off for events that the generator weightings time.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus and six maintainable components, fixed and extendable, with a dear peaker to cover them.""" + n = spine.build() + n.add('Bus', 'east') + n.add('Generator', 'hydro', bus='north', p_nom=50, marginal_cost=2, maintainable=True, maintenance_duration=3) + n.add( + 'Generator', + 'wind_ext', + bus='south', + p_nom_extendable=True, + p_nom_min=10, + p_nom_max=60, + capital_cost=20, + marginal_cost=1, + p_max_pu=[0.9, 0.6, 0.8, 0.7], + maintainable=True, + maintenance_duration=2, + maintenance_pu=0.5, + ) + n.add( + 'Link', + 'tie_fix', + bus0='north', + bus1='east', + p_nom=30, + efficiency=0.95, + maintainable=True, + maintenance_duration=1, + maintenance_events=2, + ) + n.add( + 'Link', + 'tie_ext', + bus0='south', + bus1='east', + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=40, + capital_cost=4, + efficiency=0.9, + p_min_pu=-1, + maintainable=True, + maintenance_duration=3, + ) + n.add( + 'Process', + 'conv_fix', + bus0='north', + bus1='east', + rate0=-1.25, + p_nom=25, + maintainable=True, + maintenance_duration=3, + maintenance_pu=0.6, + ) + n.add( + 'Process', + 'conv_ext', + bus0='south', + bus1='east', + rate0=-1.25, + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=30, + capital_cost=3, + maintainable=True, + maintenance_duration=1, + ) + n.add('Generator', 'east_peak', bus='east', p_nom=100, marginal_cost=60) + n.add('Load', 'east_load', bus='east', p_set=[50, 60, 40, 55]) + return n +``` + +
+ + +### Rung 34 — maintenance of committable units + +A committable build scales its bounds by the status, so maintenance takes its +share off the status. PyPSA writes the product of the status and `maintenance` +as `maintenance_status`, with three McCormick rows, so a unit in maintenance +can also be off (`variables.py:301-338`, `constraints.py:425-458`). A modular +committable build uses the same product, bounded by the module count +`p_nom_max / p_nom_mod` (`constraints.py:498-533`). An extendable committable +build that is not modular uses `maintenance_capacity` in its big-M rows +instead (`constraints.py:363-373`). `-com-ext-p-upper-bigM` does not change. +Ramps, the up and down times and the storage rows do not read maintenance. + +With the same network and no `maintainable`, PyPSA solves to `5370.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Generator-maintenance_status`](#variable-domains), and the `Link` and `Process` ones | done | | +| [`Generator-maint-status-*`](#generator-maint-status-le-status), [`-maint-modstatus-*`](#generator-maint-modstatus-le-status), and the `Link` and `Process` ones | done | a fixed build's status, and a modular build's module count | +| [`Generator-com-p-*`](#generator-com-p-lower), [`-com-mod-p-*`](#generator-com-mod-p-lower), [`-com-ext-p-lower`](#generator-com-ext-p-lower), [`-com-ext-p-upper-cap`](#generator-com-ext-p-upper-cap), and the `Link` and `Process` ones, in maintenance | done | | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `6420.048611111111`, 503 rows. + +
+The network, as PyPSA code + +`rung_34_committable_maintenance.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 34: maintenance of committable units — a fixed, an extendable and a modular committable generator, link and process, each taken off for one event.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus and nine maintainable committable components, with a dear peaker to cover them.""" + n = spine.build() + n.add('Bus', 'east') + committable = {'committable': True, 'maintainable': True} + n.add( + 'Generator', + 'unit', + bus='north', + p_nom=60, + p_min_pu=0.3, + marginal_cost=5, + start_up_cost=10, + maintenance_duration=2, + **committable, + ) + n.add( + 'Generator', + 'unit_ext', + bus='south', + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=50, + capital_cost=10, + p_min_pu=0.2, + marginal_cost=4, + maintenance_duration=3, + maintenance_pu=0.5, + **committable, + ) + n.add( + 'Generator', + 'unit_mod', + bus='south', + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=30, + capital_cost=8, + p_min_pu=0.5, + marginal_cost=3, + maintenance_duration=1, + **committable, + ) + for component, ports in (('Link', {}), ('Process', {'rate0': -1.25})): + n.add( + component, + f'{component.lower()}_unit', + bus0='north', + bus1='east', + p_nom=40, + p_min_pu=0.2, + marginal_cost=1, + maintenance_duration=2, + **ports, + **committable, + ) + n.add( + component, + f'{component.lower()}_ext', + bus0='south', + bus1='east', + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=30, + capital_cost=5, + p_min_pu=0.2, + maintenance_duration=3, + maintenance_pu=0.5, + **ports, + **committable, + ) + n.add( + component, + f'{component.lower()}_mod', + bus0='north', + bus1='east', + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=20, + capital_cost=3, + p_min_pu=0.5, + maintenance_duration=1, + **ports, + **committable, + ) + n.add('Generator', 'east_peak', bus='east', p_nom=100, marginal_cost=60) + n.add('Load', 'east_load', bus='east', p_set=[50, 60, 40, 55]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2654,6 +2920,8 @@ data prep, or harness — is one open question. Line numbers are pinned pypsa | `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` and `Transformer_loss_max` | X4 | | `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | | `ValueError`, `abstract.py:427`, `:445` | a security-constrained run over scenarios | rows per scenario, not refused | | +| `ConsistencyError`, `consistency.py:1506-1560` | a maintainable component whose `maintenance_duration` or `maintenance_events` is not positive, whose events do not fit the weighted horizon, or that is extendable with `p_nom_max = inf` | data prep, at `*_maintenance_cover` and `*_maintenance_start_blocked`; an infinite coefficient in the `maintcap` rows | | +| nothing; HiGHS refuses the model, `constraints.py:500-503` | a fixed modular committable maintainable build, whose module count `p_nom_max / p_nom_mod` is infinite | an infinite coefficient in the `maint-modstatus` rows | | Duals and solutions are read back by the harness on the lpspec side: `marginal_price` is the balance dual over `w_objective`, `mu_upper` the @@ -2669,12 +2937,12 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y},\ \mathrm{Generator\_maintenance\_cover} \subseteq \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Link\_maintenance\_cover} \subseteq \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Process\_maintenance\_cover} \subseteq \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$ — dispatch periods | | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Generator\_maintenance\_cover} \subseteq \mathcal{G} \times \mathcal{T} \times \mathcal{T}`$ — generating units, each on one bus | +| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_maintenance\_cover} \subseteq \mathcal{L} \times \mathcal{T} \times \mathcal{T}`$ — controllable connections, each from one bus to the buses it delivers to | | $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | +| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_maintenance\_cover} \subseteq \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | | $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | | $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | @@ -2716,6 +2984,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{N}^{\mathrm{fix}}`$ | `Generator_modules_installed` over $`\mathcal{G}`$ — how many whole modules a committable build has in place: `Generator_p_nom / Generator_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | | $`\mathrm{M}`$ | `Generator_big_m` over $`\mathcal{G}`$ — a bound safely above any feasible output — the build cap at full availability, data prep | | $`\mathrm{nonneg}`$ | `Generator_p_min_pu_nonneg` over $`\mathcal{G}`$ — true where none of the generator's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | +| $`\mathrm{mnt}`$ | `Generator_maintainable` over $`\mathcal{G}`$ — whether a generator must be taken off for maintenance within the horizon | +| $`\gamma`$ | `Generator_maintenance_pu` over $`\mathcal{G}`$ — the share of the build a maintenance event takes off | +| $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\mathcal{G}`$ — how many maintenance events the horizon holds | +| $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit | | $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit | | $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\mathcal{L}`$ — nominal power | @@ -2742,6 +3014,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{N}^{f,\mathrm{fix}}`$ | `Link_modules_installed` over $`\mathcal{L}`$ — how many whole modules a committable build has in place: `Link_p_nom / Link_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | | $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\mathcal{L}`$ — a bound safely above any feasible flow — the build cap at full availability, data prep | | $`\mathrm{nonneg}^{f}`$ | `Link_p_min_pu_nonneg` over $`\mathcal{L}`$ — true where none of the link's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | +| $`\mathrm{mnt}^{f}`$ | `Link_maintainable` over $`\mathcal{L}`$ — whether a link must be taken off for maintenance within the horizon | +| $`\gamma^{f}`$ | `Link_maintenance_pu` over $`\mathcal{L}`$ — the share of the build a maintenance event takes off | +| $`\mathrm{n}^{f,\mathrm{mnt}}`$ | `Link_maintenance_events` over $`\mathcal{L}`$ — how many maintenance events the horizon holds | +| $`\mathrm{blk}^{f}`$ | `Link_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{L}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the link does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\mathcal{J}`$ — nominal internal power | | $`\mathrm{ext}^{z}`$ | `Process_p_nom_extendable` over $`\mathcal{J}`$ — whether the nominal internal power is a decision | | $`\underline{\mathrm{z}}`$ | `Process_p_min_pu` over $`\mathcal{T} \times \mathcal{J}`$ — least internal power, per unit of nominal power — negative for a process that runs both ways | @@ -2772,6 +3048,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{N}^{z,\mathrm{fix}}`$ | `Process_modules_installed` over $`\mathcal{J}`$ — how many whole modules a committable build has in place: `Process_p_nom / Process_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | | $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\mathcal{J}`$ — a bound safely above any feasible internal power — the build cap at full availability, data prep | | $`\mathrm{nonneg}^{z}`$ | `Process_p_min_pu_nonneg` over $`\mathcal{J}`$ — true where none of the process's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | +| $`\mathrm{mnt}^{z}`$ | `Process_maintainable` over $`\mathcal{J}`$ — whether a process must be taken off for maintenance within the horizon | +| $`\gamma^{z}`$ | `Process_maintenance_pu` over $`\mathcal{J}`$ — the share of the build a maintenance event takes off | +| $`\mathrm{n}^{z,\mathrm{mnt}}`$ | `Process_maintenance_events` over $`\mathcal{J}`$ — how many maintenance events the horizon holds | +| $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | @@ -2926,14 +3206,26 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`u`$ | `Generator_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-status` — how much of a committable unit is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}`$ | `Generator_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | | $`\mathit{dn}`$ | `Generator_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | +| $`\mu`$ | `Generator_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance` — whether a maintainable generator is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{\mathrm{up}}`$ | `Generator_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{\mathrm{nom}}`$ | `Generator_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{u}`$ | `Generator_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | | $`N^{f}`$ | `Link_n_mod` over $`\mathcal{L}`$ — `Link-n_mod` — how many modules of an extendable modular build | | $`u^{f}`$ | `Link_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-status` — how much of a committable link is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}^{f}`$ | `Link_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-start_up` — how much of a committable link turns on this snapshot, capped as the status is | | $`\mathit{dn}^{f}`$ | `Link_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-shut_down` — how much of a committable link turns off this snapshot, capped as the status is | +| $`\mu^{f}`$ | `Link_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance` — whether a maintainable link is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{f,\mathrm{up}}`$ | `Link_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{f,\mathrm{nom}}`$ | `Link_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{f,u}`$ | `Link_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | | $`N^{z}`$ | `Process_n_mod` over $`\mathcal{J}`$ — `Process-n_mod` — how many modules of an extendable modular build | | $`u^{z}`$ | `Process_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-status` — how much of a committable process is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}^{z}`$ | `Process_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-start_up` — how much of a committable process turns on this snapshot, capped as the status is | | $`\mathit{dn}^{z}`$ | `Process_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-shut_down` — how much of a committable process turns off this snapshot, capped as the status is | +| $`\mu^{z}`$ | `Process_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance` — whether a maintainable process is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{z,\mathrm{up}}`$ | `Process_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{z,\mathrm{nom}}`$ | `Process_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{z,u}`$ | `Process_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | | $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | @@ -3042,11 +3334,11 @@ Generator_fix_p_lower: description: "`Generator-fix-p-lower` — a fixed generator outputs at least its minimum" dims: [scenario, snapshot, generator] where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( 1 - \gamma_{g} \cdot \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-fix-p-upper` @@ -3058,11 +3350,11 @@ Generator_fix_p_upper: description: "`Generator-fix-p-upper` — a fixed generator outputs at most what is available" dims: [scenario, snapshot, generator] where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( 1 - \gamma_{g} \cdot \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Link-fix-p-lower` @@ -3074,11 +3366,11 @@ Link_fix_p_lower: description: "`Link-fix-p-lower` — a fixed link carries at least its minimum, negative for the other way" dims: [scenario, snapshot, link] where: not Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom + expression: Link_p >= Link_p_min_pu * Link_p_nom * (1 - Link_maintenance_pu * Link_maintenance) ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( 1 - \gamma^{f}_{l} \cdot \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-fix-p-upper` @@ -3090,11 +3382,11 @@ Link_fix_p_upper: description: "`Link-fix-p-upper` — a fixed link carries at most its nominal power" dims: [scenario, snapshot, link] where: not Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom + expression: Link_p <= Link_p_max_pu * Link_p_nom * (1 - Link_maintenance_pu * Link_maintenance) ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( 1 - \gamma^{f}_{l} \cdot \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Generator-ext-p-lower` @@ -3106,11 +3398,11 @@ Generator_ext_p_lower: description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" dims: [scenario, snapshot, generator] where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext + expression: Generator_p >= Generator_p_min_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-ext-p-upper` @@ -3122,11 +3414,11 @@ Generator_ext_p_upper: description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" dims: [scenario, snapshot, generator] where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext + expression: Generator_p <= Generator_p_max_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-ext-p_nom-lower` @@ -3218,11 +3510,11 @@ Link_ext_p_lower: description: "`Link-ext-p-lower` — an extendable link carries at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, link] where: Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom_ext + expression: Link_p >= Link_p_min_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p-upper` @@ -3234,11 +3526,11 @@ Link_ext_p_upper: description: "`Link-ext-p-upper` — an extendable link carries at most the chosen build" dims: [scenario, snapshot, link] where: Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom_ext + expression: Link_p <= Link_p_max_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p_nom-lower` @@ -3298,11 +3590,11 @@ Process_fix_p_lower: description: "`Process-fix-p-lower` — a fixed process runs at least its minimum, negative for the other way" dims: [scenario, snapshot, process] where: not Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom + expression: Process_p >= Process_p_min_pu * Process_p_nom * (1 - Process_maintenance_pu * Process_maintenance) ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( 1 - \gamma^{z}_{j} \cdot \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-fix-p-upper` @@ -3314,11 +3606,11 @@ Process_fix_p_upper: description: "`Process-fix-p-upper` — a fixed process runs at most its nominal power" dims: [scenario, snapshot, process] where: not Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom + expression: Process_p <= Process_p_max_pu * Process_p_nom * (1 - Process_maintenance_pu * Process_maintenance) ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( 1 - \gamma^{z}_{j} \cdot \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p-lower` @@ -3330,11 +3622,11 @@ Process_ext_p_lower: description: "`Process-ext-p-lower` — an extendable process runs at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, process] where: Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom_ext + expression: Process_p >= Process_p_min_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p-upper` @@ -3346,11 +3638,11 @@ Process_ext_p_upper: description: "`Process-ext-p-upper` — an extendable process runs at most the chosen build" dims: [scenario, snapshot, process] where: Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom_ext + expression: Process_p <= Process_p_max_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p_nom-lower` @@ -3508,11 +3800,11 @@ Generator_com_p_lower: description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing" dims: [scenario, snapshot, generator] where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * Generator_status + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-p-upper` @@ -3524,11 +3816,11 @@ Generator_com_p_upper: description: "`Generator-com-p-upper` — a committed unit outputs at most what is available; off, at most nothing" dims: [scenario, snapshot, generator] where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * Generator_status + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-transition-start-up` @@ -3766,11 +4058,11 @@ Generator_com_ext_p_upper_cap: at most what is available of the chosen build, whatever its status dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext + expression: Generator_p <= Generator_p_max_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot P_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-upper-bigM` @@ -3802,12 +4094,12 @@ Generator_com_ext_p_lower: where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: >- Generator_p >= - Generator_p_min_pu * Generator_p_nom_ext + Generator_p_min_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) + Generator_big_m * Generator_status - Generator_big_m ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot P_{g} + \mathrm{M}_{g} \cdot u_{\xi,t,g} - \mathrm{M}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) + \mathrm{M}_{g} \cdot u_{\xi,t,g} - \mathrm{M}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-lower-nonneg` @@ -3842,11 +4134,11 @@ Generator_com_mod_p_lower: its minimum of one module, whether the build is fixed or a decision dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_mod * Generator_status + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_mod * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-mod-p-upper` @@ -3860,11 +4152,11 @@ Generator_com_mod_p_upper: one module's share, whether the build is fixed or a decision dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_mod * Generator_status + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_mod * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-status-p-fixed-upper` @@ -3972,6 +4264,221 @@ Generator_shut_down_p_nom_variable_upper: \mathit{dn}_{\xi,t,g} \le N_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` +### `Generator-maint-event-count` + +`Generator_maint_event_count` + +```yaml +Generator_maint_event_count: + description: "`Generator-maint-event-count` — a maintainable generator holds its number of maintenance events over the horizon" + dims: [scenario, generator] + where: Generator_maintainable + expression: sum(Generator_maintenance_start, over=snapshot) == Generator_maintenance_events +``` + +```math +\sum_{t \in \mathcal{T}} \mu^{\mathrm{up}}_{\xi,t,g} = \mathrm{n}^{\mathrm{mnt}}_{g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator-maint-window` + +`Generator_maint_window` + +```yaml +Generator_maint_window: + description: >- + `Generator-maint-window` — a generator is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_active + expression: Generator_maintenance == sum(Generator_maintenance_start, by=Generator_maintenance_cover, over=start, into=covered) +``` + +```math +\mu_{\xi,t,g} = \sum_{t' \in \mathcal{T} \,:\, \left( g,\ t',\ t \right) \in \mathrm{Generator\_maintenance\_cover}} \mu^{\mathrm{up}}_{\xi,t',g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maint-start-horizon` + +`Generator_maint_start_horizon` + +```yaml +Generator_maint_start_horizon: + description: "`Generator-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_active AND Generator_maintenance_start_blocked + expression: Generator_maintenance_start == 0 +``` + +```math +\mu^{\mathrm{up}}_{\xi,t,g} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} \wedge \mathrm{blk}_{t,g} +``` + +### `Generator-maintcap_upper` + +`Generator_maintcap_upper` + +```yaml +Generator_maintcap_upper: + description: >- + `Generator-maintcap_upper` — the build taken off is at most the chosen build in + maintenance, and at most the build less its floor out of it + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active + expression: Generator_maintenance_capacity <= Generator_p_nom_ext - Generator_p_nom_min * (1 - Generator_maintenance) +``` + +```math +\mu^{\mathrm{nom}}_{\xi,t,g} \le P_{g} - \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maintcap_upper_nommax` + +`Generator_maintcap_upper_nommax` + +```yaml +Generator_maintcap_upper_nommax: + description: "`Generator-maintcap_upper_nommax` — out of maintenance, no build is taken off" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active + expression: Generator_maintenance_capacity <= Generator_p_nom_max * Generator_maintenance +``` + +```math +\mu^{\mathrm{nom}}_{\xi,t,g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maintcap_lower_nommax` + +`Generator_maintcap_lower_nommax` + +```yaml +Generator_maintcap_lower_nommax: + description: "`Generator-maintcap_lower_nommax` — in maintenance, the whole chosen build is taken off" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active + expression: Generator_maintenance_capacity >= Generator_p_nom_ext - Generator_p_nom_max * (1 - Generator_maintenance) +``` + +```math +\mu^{\mathrm{nom}}_{\xi,t,g} \ge P_{g} - \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maintcap_lower_nommin` + +`Generator_maintcap_lower_nommin` + +```yaml +Generator_maintcap_lower_nommin: + description: "`Generator-maintcap_lower_nommin` — in maintenance, at least the floor of the build is taken off" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active AND Generator_p_nom_min > 0 + expression: Generator_maintenance_capacity >= Generator_p_nom_min * Generator_maintenance +``` + +```math +\mu^{\mathrm{nom}}_{\xi,t,g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} \wedge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} > 0 +``` + +### `Generator-maint-status-le-status` + +`Generator_maint_status_le_status` + +```yaml +Generator_maint_status_le_status: + description: "`Generator-maint-status-le-status` — the status in maintenance is at most the status" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_active + expression: Generator_maintenance_status <= Generator_status +``` + +```math +\mu^{u}_{\xi,t,g} \le u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maint-status-le-maint` + +`Generator_maint_status_le_maint` + +```yaml +Generator_maint_status_le_maint: + description: "`Generator-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_active + expression: Generator_maintenance_status <= Generator_maintenance +``` + +```math +\mu^{u}_{\xi,t,g} \le \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maint-status-lb` + +`Generator_maint_status_lb` + +```yaml +Generator_maint_status_lb: + description: "`Generator-maint-status-lb` — on and in maintenance, the status in maintenance is one" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_active + expression: Generator_maintenance_status >= Generator_status + Generator_maintenance - 1 +``` + +```math +\mu^{u}_{\xi,t,g} \ge u_{\xi,t,g} + \mu_{\xi,t,g} - 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maint-modstatus-le-status` + +`Generator_maint_modstatus_le_status` + +```yaml +Generator_maint_modstatus_le_status: + description: "`Generator-maint-modstatus-le-status` — the modules on in maintenance are at most the modules on" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active + expression: Generator_maintenance_status <= Generator_status +``` + +```math +\mu^{u}_{\xi,t,g} \le u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maint-modstatus-le-maint` + +`Generator_maint_modstatus_le_maint` + +```yaml +Generator_maint_modstatus_le_maint: + description: >- + `Generator-maint-modstatus-le-maint` — out of maintenance, no module is on in + maintenance; in it, at most the modules the build cap holds + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active + expression: Generator_maintenance_status <= Generator_p_nom_max / Generator_p_nom_mod * Generator_maintenance +``` + +```math +\mu^{u}_{\xi,t,g} \le \frac{\overline{\mathrm{p}}^{\mathrm{nom}}_{g}}{\mathrm{p}^{\mathrm{mod}}_{g}} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +``` + +### `Generator-maint-modstatus-lb` + +`Generator_maint_modstatus_lb` + +```yaml +Generator_maint_modstatus_lb: + description: "`Generator-maint-modstatus-lb` — in maintenance, every module on is on in maintenance" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active + expression: Generator_maintenance_status >= Generator_status - Generator_p_nom_max / Generator_p_nom_mod * (1 - Generator_maintenance) +``` + +```math +\mu^{u}_{\xi,t,g} \ge u_{\xi,t,g} - \frac{\overline{\mathrm{p}}^{\mathrm{nom}}_{g}}{\mathrm{p}^{\mathrm{mod}}_{g}} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +``` + ### `Link-com-p-lower` `Link_com_p_lower` @@ -3981,11 +4488,11 @@ Link_com_p_lower: description: "`Link-com-p-lower` — a committed link flows at least its minimum; off, at least nothing" dims: [scenario, snapshot, link] where: Link_committable AND not Link_p_nom_extendable AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom * Link_status + expression: Link_p >= Link_p_min_pu * Link_p_nom * (Link_status - Link_maintenance_pu * Link_maintenance_status) ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-p-upper` @@ -3997,11 +4504,11 @@ Link_com_p_upper: description: "`Link-com-p-upper` — a committed link flows at most what is available; off, at most nothing" dims: [scenario, snapshot, link] where: Link_committable AND not Link_p_nom_extendable AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom * Link_status + expression: Link_p <= Link_p_max_pu * Link_p_nom * (Link_status - Link_maintenance_pu * Link_maintenance_status) ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-transition-start-up` @@ -4239,11 +4746,11 @@ Link_com_ext_p_upper_cap: at most what is available of the chosen build, whatever its status dims: [scenario, snapshot, link] where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom_ext + expression: Link_p <= Link_p_max_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot F_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-ext-p-upper-bigM` @@ -4259,190 +4766,405 @@ Link_com_ext_p_upper_big_m: ``` ```math -f_{\xi,t,l} \le \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-ext-p-lower` + +`Link_com_ext_p_lower` + +```yaml +Link_com_ext_p_lower: + description: >- + `Link-com-ext-p-lower` — a committed extendable link flows at + least its minimum of the chosen build; off, the big M releases the row + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: >- + Link_p >= + Link_p_min_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) + + Link_big_m * Link_status - Link_big_m +``` + +```math +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) + \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} - \mathrm{M}^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-ext-p-lower-nonneg` + +`Link_com_ext_p_lower_nonneg` + +```yaml +Link_com_ext_p_lower_nonneg: + description: >- + `Link-com-ext-p-lower-nonneg` — where no minimum-per-unit is + negative, flow is also plainly non-negative, a row the big-M lower + cannot assert while the link is off + dims: [scenario, snapshot, link] + where: >- + Link_committable AND Link_p_nom_extendable + AND Link_p_min_pu_nonneg AND NOT (Link_p_nom_mod > 0) AND Link_active + expression: Link_p >= 0 +``` + +```math +f_{\xi,t,l} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{nonneg}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-mod-p-lower` + +`Link_com_mod_p_lower` + +```yaml +Link_com_mod_p_lower: + description: >- + `Link-com-mod-p-lower` — a committed modular link flows at least + its minimum of one module, whether the build is fixed or a decision + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_p >= Link_p_min_pu * Link_p_nom_mod * (Link_status - Link_maintenance_pu * Link_maintenance_status) +``` + +```math +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-com-mod-p-upper` + +`Link_com_mod_p_upper` + +```yaml +Link_com_mod_p_upper: + description: >- + `Link-com-mod-p-upper` — a committed modular link flows at most + one module's share, whether the build is fixed or a decision + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_p <= Link_p_max_pu * Link_p_nom_mod * (Link_status - Link_maintenance_pu * Link_maintenance_status) +``` + +```math +f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-status-p-fixed-upper` + +`Link_status_p_fixed_upper` + +```yaml +Link_status_p_fixed_upper: + description: >- + `Link-status-p-fixed-upper` — a status is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_status <= Link_modules_installed +``` + +```math +u^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-start_up-p-fixed-upper` + +`Link_start_up_p_fixed_upper` + +```yaml +Link_start_up_p_fixed_upper: + description: >- + `Link-start_up-p-fixed-upper` — a start is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_start_up <= Link_modules_installed +``` + +```math +\mathit{up}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-shut_down-p-fixed-upper` + +`Link_shut_down_p_fixed_upper` + +```yaml +Link_shut_down_p_fixed_upper: + description: >- + `Link-shut_down-p-fixed-upper` — a stop is at most the modules in + place, an explicit row as PyPSA writes it: one where the build is not + modular, and the fixed build's whole count of modules where it is + dims: [scenario, snapshot, link] + where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_shut_down <= Link_modules_installed +``` + +```math +\mathit{dn}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-status-p_nom-variable-upper` + +`Link_status_p_nom_variable_upper` + +```yaml +Link_status_p_nom_variable_upper: + description: "`Link-status-p_nom-variable-upper` — a modular link is on only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_status <= Link_n_mod +``` + +```math +u^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-start_up-p_nom-variable-upper` + +`Link_start_up_p_nom_variable_upper` + +```yaml +Link_start_up_p_nom_variable_upper: + description: "`Link-start_up-p_nom-variable-upper` — a modular link starts only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_start_up <= Link_n_mod +``` + +```math +\mathit{up}^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-shut_down-p_nom-variable-upper` + +`Link_shut_down_p_nom_variable_upper` + +```yaml +Link_shut_down_p_nom_variable_upper: + description: "`Link-shut_down-p_nom-variable-upper` — a modular link stops only where a module is built" + dims: [scenario, snapshot, link] + where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_shut_down <= Link_n_mod +``` + +```math +\mathit{dn}^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-maint-event-count` + +`Link_maint_event_count` + +```yaml +Link_maint_event_count: + description: "`Link-maint-event-count` — a maintainable link holds its number of maintenance events over the horizon" + dims: [scenario, link] + where: Link_maintainable + expression: sum(Link_maintenance_start, over=snapshot) == Link_maintenance_events +``` + +```math +\sum_{t \in \mathcal{T}} \mu^{f,\mathrm{up}}_{\xi,t,l} = \mathrm{n}^{f,\mathrm{mnt}}_{l} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +``` + +### `Link-maint-window` + +`Link_maint_window` + +```yaml +Link_maint_window: + description: >- + `Link-maint-window` — a link is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_active + expression: Link_maintenance == sum(Link_maintenance_start, by=Link_maintenance_cover, over=start, into=covered) +``` + +```math +\mu^{f}_{\xi,t,l} = \sum_{t' \in \mathcal{T} \,:\, \left( l,\ t',\ t \right) \in \mathrm{Link\_maintenance\_cover}} \mu^{f,\mathrm{up}}_{\xi,t',l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +### `Link-maint-start-horizon` + +`Link_maint_start_horizon` + +```yaml +Link_maint_start_horizon: + description: "`Link-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_active AND Link_maintenance_start_blocked + expression: Link_maintenance_start == 0 +``` + +```math +\mu^{f,\mathrm{up}}_{\xi,t,l} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} \wedge \mathrm{blk}^{f}_{t,l} ``` -### `Link-com-ext-p-lower` +### `Link-maintcap_upper` -`Link_com_ext_p_lower` +`Link_maintcap_upper` ```yaml -Link_com_ext_p_lower: +Link_maintcap_upper: description: >- - `Link-com-ext-p-lower` — a committed extendable link flows at - least its minimum of the chosen build; off, the big M releases the row + `Link-maintcap_upper` — the build taken off is at most the chosen build in + maintenance, and at most the build less its floor out of it dims: [scenario, snapshot, link] - where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active - expression: >- - Link_p >= - Link_p_min_pu * Link_p_nom_ext - + Link_big_m * Link_status - Link_big_m + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_maintenance_capacity <= Link_p_nom_ext - Link_p_nom_min * (1 - Link_maintenance) ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot F_{l} + \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} - \mathrm{M}^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,\mathrm{nom}}_{\xi,t,l} \le F_{l} - \underline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-com-ext-p-lower-nonneg` +### `Link-maintcap_upper_nommax` -`Link_com_ext_p_lower_nonneg` +`Link_maintcap_upper_nommax` ```yaml -Link_com_ext_p_lower_nonneg: - description: >- - `Link-com-ext-p-lower-nonneg` — where no minimum-per-unit is - negative, flow is also plainly non-negative, a row the big-M lower - cannot assert while the link is off +Link_maintcap_upper_nommax: + description: "`Link-maintcap_upper_nommax` — out of maintenance, no build is taken off" dims: [scenario, snapshot, link] - where: >- - Link_committable AND Link_p_nom_extendable - AND Link_p_min_pu_nonneg AND NOT (Link_p_nom_mod > 0) AND Link_active - expression: Link_p >= 0 + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_maintenance_capacity <= Link_p_nom_max * Link_maintenance ``` ```math -f_{\xi,t,l} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{nonneg}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,\mathrm{nom}}_{\xi,t,l} \le \overline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-com-mod-p-lower` +### `Link-maintcap_lower_nommax` -`Link_com_mod_p_lower` +`Link_maintcap_lower_nommax` ```yaml -Link_com_mod_p_lower: - description: >- - `Link-com-mod-p-lower` — a committed modular link flows at least - its minimum of one module, whether the build is fixed or a decision +Link_maintcap_lower_nommax: + description: "`Link-maintcap_lower_nommax` — in maintenance, the whole chosen build is taken off" dims: [scenario, snapshot, link] - where: Link_committable AND Link_p_nom_mod > 0 AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom_mod * Link_status + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_maintenance_capacity >= Link_p_nom_ext - Link_p_nom_max * (1 - Link_maintenance) ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,\mathrm{nom}}_{\xi,t,l} \ge F_{l} - \overline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-com-mod-p-upper` +### `Link-maintcap_lower_nommin` -`Link_com_mod_p_upper` +`Link_maintcap_lower_nommin` ```yaml -Link_com_mod_p_upper: - description: >- - `Link-com-mod-p-upper` — a committed modular link flows at most - one module's share, whether the build is fixed or a decision +Link_maintcap_lower_nommin: + description: "`Link-maintcap_lower_nommin` — in maintenance, at least the floor of the build is taken off" dims: [scenario, snapshot, link] - where: Link_committable AND Link_p_nom_mod > 0 AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom_mod * Link_status + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active AND Link_p_nom_min > 0 + expression: Link_maintenance_capacity >= Link_p_nom_min * Link_maintenance ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,\mathrm{nom}}_{\xi,t,l} \ge \underline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} \wedge \underline{\mathrm{f}}^{\mathrm{nom}}_{l} > 0 ``` -### `Link-status-p-fixed-upper` +### `Link-maint-status-le-status` -`Link_status_p_fixed_upper` +`Link_maint_status_le_status` ```yaml -Link_status_p_fixed_upper: - description: >- - `Link-status-p-fixed-upper` — a status is at most the modules in - place, an explicit row as PyPSA writes it: one where the build is not - modular, and the fixed build's whole count of modules where it is +Link_maint_status_le_status: + description: "`Link-maint-status-le-status` — the status in maintenance is at most the status" dims: [scenario, snapshot, link] - where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active - expression: Link_status <= Link_modules_installed + where: Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_active + expression: Link_maintenance_status <= Link_status ``` ```math -u^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \le u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-start_up-p-fixed-upper` +### `Link-maint-status-le-maint` -`Link_start_up_p_fixed_upper` +`Link_maint_status_le_maint` ```yaml -Link_start_up_p_fixed_upper: - description: >- - `Link-start_up-p-fixed-upper` — a start is at most the modules in - place, an explicit row as PyPSA writes it: one where the build is not - modular, and the fixed build's whole count of modules where it is +Link_maint_status_le_maint: + description: "`Link-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" dims: [scenario, snapshot, link] - where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active - expression: Link_start_up <= Link_modules_installed + where: Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_active + expression: Link_maintenance_status <= Link_maintenance ``` ```math -\mathit{up}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \le \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-shut_down-p-fixed-upper` +### `Link-maint-status-lb` -`Link_shut_down_p_fixed_upper` +`Link_maint_status_lb` ```yaml -Link_shut_down_p_fixed_upper: - description: >- - `Link-shut_down-p-fixed-upper` — a stop is at most the modules in - place, an explicit row as PyPSA writes it: one where the build is not - modular, and the fixed build's whole count of modules where it is +Link_maint_status_lb: + description: "`Link-maint-status-lb` — on and in maintenance, the status in maintenance is one" dims: [scenario, snapshot, link] - where: Link_committable AND NOT (Link_p_nom_extendable AND Link_p_nom_mod > 0) AND Link_active - expression: Link_shut_down <= Link_modules_installed + where: Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_active + expression: Link_maintenance_status >= Link_status + Link_maintenance - 1 ``` ```math -\mathit{dn}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \ge u^{f}_{\xi,t,l} + \mu^{f}_{\xi,t,l} - 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-status-p_nom-variable-upper` +### `Link-maint-modstatus-le-status` -`Link_status_p_nom_variable_upper` +`Link_maint_modstatus_le_status` ```yaml -Link_status_p_nom_variable_upper: - description: "`Link-status-p_nom-variable-upper` — a modular link is on only where a module is built" +Link_maint_modstatus_le_status: + description: "`Link-maint-modstatus-le-status` — the modules on in maintenance are at most the modules on" dims: [scenario, snapshot, link] - where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active - expression: Link_status <= Link_n_mod + where: Link_maintainable AND Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_maintenance_status <= Link_status ``` ```math -u^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \le u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-start_up-p_nom-variable-upper` +### `Link-maint-modstatus-le-maint` -`Link_start_up_p_nom_variable_upper` +`Link_maint_modstatus_le_maint` ```yaml -Link_start_up_p_nom_variable_upper: - description: "`Link-start_up-p_nom-variable-upper` — a modular link starts only where a module is built" +Link_maint_modstatus_le_maint: + description: >- + `Link-maint-modstatus-le-maint` — out of maintenance, no module is on in + maintenance; in it, at most the modules the build cap holds dims: [scenario, snapshot, link] - where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active - expression: Link_start_up <= Link_n_mod + where: Link_maintainable AND Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_maintenance_status <= Link_p_nom_max / Link_p_nom_mod * Link_maintenance ``` ```math -\mathit{up}^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \le \frac{\overline{\mathrm{f}}^{\mathrm{nom}}_{l}}{\mathrm{f}^{\mathrm{mod}}_{l}} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` -### `Link-shut_down-p_nom-variable-upper` +### `Link-maint-modstatus-lb` -`Link_shut_down_p_nom_variable_upper` +`Link_maint_modstatus_lb` ```yaml -Link_shut_down_p_nom_variable_upper: - description: "`Link-shut_down-p_nom-variable-upper` — a modular link stops only where a module is built" +Link_maint_modstatus_lb: + description: "`Link-maint-modstatus-lb` — in maintenance, every module on is on in maintenance" dims: [scenario, snapshot, link] - where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active - expression: Link_shut_down <= Link_n_mod + where: Link_maintainable AND Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_maintenance_status >= Link_status - Link_p_nom_max / Link_p_nom_mod * (1 - Link_maintenance) ``` ```math -\mathit{dn}^{f}_{\xi,t,l} \le N^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \ge u^{f}_{\xi,t,l} - \frac{\overline{\mathrm{f}}^{\mathrm{nom}}_{l}}{\mathrm{f}^{\mathrm{mod}}_{l}} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-com-p-lower` @@ -4454,11 +5176,11 @@ Process_com_p_lower: description: "`Process-com-p-lower` — a committed process runs at least its minimum; off, at least nothing" dims: [scenario, snapshot, process] where: Process_committable AND not Process_p_nom_extendable AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom * Process_status + expression: Process_p >= Process_p_min_pu * Process_p_nom * (Process_status - Process_maintenance_pu * Process_maintenance_status) ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-p-upper` @@ -4470,11 +5192,11 @@ Process_com_p_upper: description: "`Process-com-p-upper` — a committed process runs at most what is available; off, at most nothing" dims: [scenario, snapshot, process] where: Process_committable AND not Process_p_nom_extendable AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom * Process_status + expression: Process_p <= Process_p_max_pu * Process_p_nom * (Process_status - Process_maintenance_pu * Process_maintenance_status) ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-transition-start-up` @@ -4712,11 +5434,11 @@ Process_com_ext_p_upper_cap: at most what is available of the chosen build, whatever its status dims: [scenario, snapshot, process] where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom_ext + expression: Process_p <= Process_p_max_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot Z_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-ext-p-upper-bigM` @@ -4748,12 +5470,12 @@ Process_com_ext_p_lower: where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active expression: >- Process_p >= - Process_p_min_pu * Process_p_nom_ext + Process_p_min_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) + Process_big_m * Process_status - Process_big_m ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot Z_{j} + \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} - \mathrm{M}^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) + \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} - \mathrm{M}^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-ext-p-lower-nonneg` @@ -4788,11 +5510,11 @@ Process_com_mod_p_lower: its minimum of one module, whether the build is fixed or a decision dims: [scenario, snapshot, process] where: Process_committable AND Process_p_nom_mod > 0 AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom_mod * Process_status + expression: Process_p >= Process_p_min_pu * Process_p_nom_mod * (Process_status - Process_maintenance_pu * Process_maintenance_status) ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-mod-p-upper` @@ -4806,11 +5528,11 @@ Process_com_mod_p_upper: one module's share, whether the build is fixed or a decision dims: [scenario, snapshot, process] where: Process_committable AND Process_p_nom_mod > 0 AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom_mod * Process_status + expression: Process_p <= Process_p_max_pu * Process_p_nom_mod * (Process_status - Process_maintenance_pu * Process_maintenance_status) ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-status-p-fixed-upper` @@ -4918,6 +5640,221 @@ Process_shut_down_p_nom_variable_upper: \mathit{dn}^{z}_{\xi,t,j} \le N^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} ``` +### `Process-maint-event-count` + +`Process_maint_event_count` + +```yaml +Process_maint_event_count: + description: "`Process-maint-event-count` — a maintainable process holds its number of maintenance events over the horizon" + dims: [scenario, process] + where: Process_maintainable + expression: sum(Process_maintenance_start, over=snapshot) == Process_maintenance_events +``` + +```math +\sum_{t \in \mathcal{T}} \mu^{z,\mathrm{up}}_{\xi,t,j} = \mathrm{n}^{z,\mathrm{mnt}}_{j} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +``` + +### `Process-maint-window` + +`Process_maint_window` + +```yaml +Process_maint_window: + description: >- + `Process-maint-window` — a process is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_active + expression: Process_maintenance == sum(Process_maintenance_start, by=Process_maintenance_cover, over=start, into=covered) +``` + +```math +\mu^{z}_{\xi,t,j} = \sum_{t' \in \mathcal{T} \,:\, \left( j,\ t',\ t \right) \in \mathrm{Process\_maintenance\_cover}} \mu^{z,\mathrm{up}}_{\xi,t',j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maint-start-horizon` + +`Process_maint_start_horizon` + +```yaml +Process_maint_start_horizon: + description: "`Process-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_active AND Process_maintenance_start_blocked + expression: Process_maintenance_start == 0 +``` + +```math +\mu^{z,\mathrm{up}}_{\xi,t,j} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} \wedge \mathrm{blk}^{z}_{t,j} +``` + +### `Process-maintcap_upper` + +`Process_maintcap_upper` + +```yaml +Process_maintcap_upper: + description: >- + `Process-maintcap_upper` — the build taken off is at most the chosen build in + maintenance, and at most the build less its floor out of it + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_maintenance_capacity <= Process_p_nom_ext - Process_p_nom_min * (1 - Process_maintenance) +``` + +```math +\mu^{z,\mathrm{nom}}_{\xi,t,j} \le Z_{j} - \underline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maintcap_upper_nommax` + +`Process_maintcap_upper_nommax` + +```yaml +Process_maintcap_upper_nommax: + description: "`Process-maintcap_upper_nommax` — out of maintenance, no build is taken off" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_maintenance_capacity <= Process_p_nom_max * Process_maintenance +``` + +```math +\mu^{z,\mathrm{nom}}_{\xi,t,j} \le \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maintcap_lower_nommax` + +`Process_maintcap_lower_nommax` + +```yaml +Process_maintcap_lower_nommax: + description: "`Process-maintcap_lower_nommax` — in maintenance, the whole chosen build is taken off" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_maintenance_capacity >= Process_p_nom_ext - Process_p_nom_max * (1 - Process_maintenance) +``` + +```math +\mu^{z,\mathrm{nom}}_{\xi,t,j} \ge Z_{j} - \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maintcap_lower_nommin` + +`Process_maintcap_lower_nommin` + +```yaml +Process_maintcap_lower_nommin: + description: "`Process-maintcap_lower_nommin` — in maintenance, at least the floor of the build is taken off" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active AND Process_p_nom_min > 0 + expression: Process_maintenance_capacity >= Process_p_nom_min * Process_maintenance +``` + +```math +\mu^{z,\mathrm{nom}}_{\xi,t,j} \ge \underline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} \wedge \underline{\mathrm{z}}^{\mathrm{nom}}_{j} > 0 +``` + +### `Process-maint-status-le-status` + +`Process_maint_status_le_status` + +```yaml +Process_maint_status_le_status: + description: "`Process-maint-status-le-status` — the status in maintenance is at most the status" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_active + expression: Process_maintenance_status <= Process_status +``` + +```math +\mu^{z,u}_{\xi,t,j} \le u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maint-status-le-maint` + +`Process_maint_status_le_maint` + +```yaml +Process_maint_status_le_maint: + description: "`Process-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_active + expression: Process_maintenance_status <= Process_maintenance +``` + +```math +\mu^{z,u}_{\xi,t,j} \le \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maint-status-lb` + +`Process_maint_status_lb` + +```yaml +Process_maint_status_lb: + description: "`Process-maint-status-lb` — on and in maintenance, the status in maintenance is one" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_active + expression: Process_maintenance_status >= Process_status + Process_maintenance - 1 +``` + +```math +\mu^{z,u}_{\xi,t,j} \ge u^{z}_{\xi,t,j} + \mu^{z}_{\xi,t,j} - 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maint-modstatus-le-status` + +`Process_maint_modstatus_le_status` + +```yaml +Process_maint_modstatus_le_status: + description: "`Process-maint-modstatus-le-status` — the modules on in maintenance are at most the modules on" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_maintenance_status <= Process_status +``` + +```math +\mu^{z,u}_{\xi,t,j} \le u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maint-modstatus-le-maint` + +`Process_maint_modstatus_le_maint` + +```yaml +Process_maint_modstatus_le_maint: + description: >- + `Process-maint-modstatus-le-maint` — out of maintenance, no module is on in + maintenance; in it, at most the modules the build cap holds + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_maintenance_status <= Process_p_nom_max / Process_p_nom_mod * Process_maintenance +``` + +```math +\mu^{z,u}_{\xi,t,j} \le \frac{\overline{\mathrm{z}}^{\mathrm{nom}}_{j}}{\mathrm{z}^{\mathrm{mod}}_{j}} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + +### `Process-maint-modstatus-lb` + +`Process_maint_modstatus_lb` + +```yaml +Process_maint_modstatus_lb: + description: "`Process-maint-modstatus-lb` — in maintenance, every module on is on in maintenance" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_maintenance_status >= Process_status - Process_p_nom_max / Process_p_nom_mod * (1 - Process_maintenance) +``` + +```math +\mu^{z,u}_{\xi,t,j} \ge u^{z}_{\xi,t,j} - \frac{\overline{\mathrm{z}}^{\mathrm{nom}}_{j}}{\mathrm{z}^{\mathrm{mod}}_{j}} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +``` + ### `Line-fix-s-lower` `Line_fix_s_lower` @@ -7297,6 +8234,30 @@ u_{\xi,t,g} \ge 0, u_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \ \mathit{dn}_{\xi,t,g} \ge 0, \mathit{dn}_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` +**`Generator_maintenance`** + +```math +0 \le \mu_{\xi,t,g} \le 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} +``` + +**`Generator_maintenance_start`** + +```math +\mu^{\mathrm{up}}_{\xi,t,g} \in \{0, 1\} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} +``` + +**`Generator_maintenance_capacity`** + +```math +\mu^{\mathrm{nom}}_{\xi,t,g} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +``` + +**`Generator_maintenance_status`** + +```math +\mu^{u}_{\xi,t,g} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \mathrm{on}_{t,g} +``` + **`Link_n_mod`** ```math @@ -7321,6 +8282,30 @@ u^{f}_{\xi,t,l} \ge 0, u^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \ \mathit{dn}^{f}_{\xi,t,l} \ge 0, \mathit{dn}^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` +**`Link_maintenance`** + +```math +0 \le \mu^{f}_{\xi,t,l} \le 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +**`Link_maintenance_start`** + +```math +\mu^{f,\mathrm{up}}_{\xi,t,l} \in \{0, 1\} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +``` + +**`Link_maintenance_capacity`** + +```math +\mu^{f,\mathrm{nom}}_{\xi,t,l} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +``` + +**`Link_maintenance_status`** + +```math +\mu^{f,u}_{\xi,t,l} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \mathrm{on}^{f}_{t,l} +``` + **`Process_n_mod`** ```math @@ -7345,6 +8330,30 @@ u^{z}_{\xi,t,j} \ge 0, u^{z}_{\xi,t,j} \in \mathbb{Z} \qquad \forall\, \xi \in \ \mathit{dn}^{z}_{\xi,t,j} \ge 0, \mathit{dn}^{z}_{\xi,t,j} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` +**`Process_maintenance`** + +```math +0 \le \mu^{z}_{\xi,t,j} \le 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +**`Process_maintenance_start`** + +```math +\mu^{z,\mathrm{up}}_{\xi,t,j} \in \{0, 1\} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +``` + +**`Process_maintenance_capacity`** + +```math +\mu^{z,\mathrm{nom}}_{\xi,t,j} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +``` + +**`Process_maintenance_status`** + +```math +\mu^{z,u}_{\xi,t,j} \ge 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \mathrm{on}^{z}_{t,j} +``` + **`Line_s`** ```math diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index ca421792..a400d145 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -163,6 +163,25 @@ parameters: PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep dims: [generator] dtype: bool + Generator_maintainable: + description: whether a generator must be taken off for maintenance within the horizon + dims: [generator] + dtype: bool + Generator_maintenance_pu: + description: the share of the build a maintenance event takes off + dims: [generator] + Generator_maintenance_events: + description: how many maintenance events the horizon holds + dims: [generator] + dtype: int + Generator_maintenance_start_blocked: + description: >- + true where no maintenance event may start, because the snapshots it + would cover run past the end of the horizon or into one the generator does + not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` + and the generator weightings, data prep + dims: [snapshot, generator] + dtype: bool Link_ramp_limit_up: description: most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit dims: [link] @@ -272,6 +291,25 @@ parameters: PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep dims: [link] dtype: bool + Link_maintainable: + description: whether a link must be taken off for maintenance within the horizon + dims: [link] + dtype: bool + Link_maintenance_pu: + description: the share of the build a maintenance event takes off + dims: [link] + Link_maintenance_events: + description: how many maintenance events the horizon holds + dims: [link] + dtype: int + Link_maintenance_start_blocked: + description: >- + true where no maintenance event may start, because the snapshots it + would cover run past the end of the horizon or into one the link does + not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` + and the generator weightings, data prep + dims: [snapshot, link] + dtype: bool Process_p_nom: description: nominal internal power dims: [process] @@ -395,6 +433,25 @@ parameters: PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep dims: [process] dtype: bool + Process_maintainable: + description: whether a process must be taken off for maintenance within the horizon + dims: [process] + dtype: bool + Process_maintenance_pu: + description: the share of the build a maintenance event takes off + dims: [process] + Process_maintenance_events: + description: how many maintenance events the horizon holds + dims: [process] + dtype: int + Process_maintenance_start_blocked: + description: >- + true where no maintenance event may start, because the snapshots it + would cover run past the end of the horizon or into one the process does + not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` + and the generator weightings, data prep + dims: [snapshot, process] + dtype: bool Load_p_set: description: demand dims: [scenario, snapshot, load] @@ -1012,6 +1069,27 @@ relations: description: the transformer an outage takes out; an outage of a line has no row key: outage values: transformer + Generator_maintenance_cover: + description: >- + the snapshots a maintenance event covers, by the snapshot it starts in — + the start and the snapshots after it, until their generator weightings + reach `maintenance_duration`, data prep; no rows for a generator that is + not maintainable + key: { generator: generator, start: snapshot, covered: snapshot } + Link_maintenance_cover: + description: >- + the snapshots a maintenance event covers, by the snapshot it starts in — + the start and the snapshots after it, until their generator weightings + reach `maintenance_duration`, data prep; no rows for a link that is + not maintainable + key: { link: link, start: snapshot, covered: snapshot } + Process_maintenance_cover: + description: >- + the snapshots a maintenance event covers, by the snapshot it starts in — + the start and the snapshots after it, until their generator weightings + reach `maintenance_duration`, data prep; no rows for a process that is + not maintainable + key: { process: process, start: snapshot, covered: snapshot } variables: Generator_p: @@ -1094,6 +1172,45 @@ variables: domain: integer bounds: lower: 0 + Generator_maintenance: + description: >- + `Generator-maintenance` — whether a maintainable generator is in maintenance: + continuous, and one exactly where an event covers the snapshot + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_active + absence: zero + bounds: + lower: 0 + upper: 1 + Generator_maintenance_start: + description: "`Generator-maintenance_start` — whether a maintenance event starts in this snapshot" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_active + absence: zero + domain: binary + Generator_maintenance_capacity: + description: >- + `Generator-maintenance_capacity` — the chosen build while in maintenance, zero + otherwise: the product the `maintcap` rows linearize + dims: [scenario, snapshot, generator] + where: >- + Generator_maintainable AND Generator_p_nom_extendable + AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active + absence: zero + bounds: + lower: 0 + Generator_maintenance_status: + description: >- + `Generator-maintenance_status` — the status while in maintenance, zero + otherwise: the product the `maint-status` rows linearize, so a unit in + maintenance may also be off + dims: [scenario, snapshot, generator] + where: >- + Generator_maintainable AND Generator_committable + AND NOT (Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) AND Generator_active + absence: zero + bounds: + lower: 0 Link_n_mod: description: "`Link-n_mod` — how many modules of an extendable modular build" dims: [link] @@ -1125,6 +1242,45 @@ variables: domain: integer bounds: lower: 0 + Link_maintenance: + description: >- + `Link-maintenance` — whether a maintainable link is in maintenance: + continuous, and one exactly where an event covers the snapshot + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_active + absence: zero + bounds: + lower: 0 + upper: 1 + Link_maintenance_start: + description: "`Link-maintenance_start` — whether a maintenance event starts in this snapshot" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_active + absence: zero + domain: binary + Link_maintenance_capacity: + description: >- + `Link-maintenance_capacity` — the chosen build while in maintenance, zero + otherwise: the product the `maintcap` rows linearize + dims: [scenario, snapshot, link] + where: >- + Link_maintainable AND Link_p_nom_extendable + AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active + absence: zero + bounds: + lower: 0 + Link_maintenance_status: + description: >- + `Link-maintenance_status` — the status while in maintenance, zero + otherwise: the product the `maint-status` rows linearize, so a unit in + maintenance may also be off + dims: [scenario, snapshot, link] + where: >- + Link_maintainable AND Link_committable + AND NOT (Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) AND Link_active + absence: zero + bounds: + lower: 0 Process_n_mod: description: "`Process-n_mod` — how many modules of an extendable modular build" dims: [process] @@ -1156,6 +1312,45 @@ variables: domain: integer bounds: lower: 0 + Process_maintenance: + description: >- + `Process-maintenance` — whether a maintainable process is in maintenance: + continuous, and one exactly where an event covers the snapshot + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_active + absence: zero + bounds: + lower: 0 + upper: 1 + Process_maintenance_start: + description: "`Process-maintenance_start` — whether a maintenance event starts in this snapshot" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_active + absence: zero + domain: binary + Process_maintenance_capacity: + description: >- + `Process-maintenance_capacity` — the chosen build while in maintenance, zero + otherwise: the product the `maintcap` rows linearize + dims: [scenario, snapshot, process] + where: >- + Process_maintainable AND Process_p_nom_extendable + AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active + absence: zero + bounds: + lower: 0 + Process_maintenance_status: + description: >- + `Process-maintenance_status` — the status while in maintenance, zero + otherwise: the product the `maint-status` rows linearize, so a unit in + maintenance may also be off + dims: [scenario, snapshot, process] + where: >- + Process_maintainable AND Process_committable + AND NOT (Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) AND Process_active + absence: zero + bounds: + lower: 0 Line_s: description: >- `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a @@ -1724,32 +1919,32 @@ constraints: description: "`Generator-fix-p-lower` — a fixed generator outputs at least its minimum" dims: [scenario, snapshot, generator] where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) Generator_fix_p_upper: description: "`Generator-fix-p-upper` — a fixed generator outputs at most what is available" dims: [scenario, snapshot, generator] where: not Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) Link_fix_p_lower: description: "`Link-fix-p-lower` — a fixed link carries at least its minimum, negative for the other way" dims: [scenario, snapshot, link] where: not Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom + expression: Link_p >= Link_p_min_pu * Link_p_nom * (1 - Link_maintenance_pu * Link_maintenance) Link_fix_p_upper: description: "`Link-fix-p-upper` — a fixed link carries at most its nominal power" dims: [scenario, snapshot, link] where: not Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom + expression: Link_p <= Link_p_max_pu * Link_p_nom * (1 - Link_maintenance_pu * Link_maintenance) Generator_ext_p_lower: description: "`Generator-ext-p-lower` — an extendable generator outputs at least its minimum of the chosen build" dims: [scenario, snapshot, generator] where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_ext + expression: Generator_p >= Generator_p_min_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) Generator_ext_p_upper: description: "`Generator-ext-p-upper` — an extendable generator outputs at most what is available of the chosen build" dims: [scenario, snapshot, generator] where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext + expression: Generator_p <= Generator_p_max_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) Generator_ext_p_nom_lower: description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" dims: [generator] @@ -1779,12 +1974,12 @@ constraints: description: "`Link-ext-p-lower` — an extendable link carries at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, link] where: Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom_ext + expression: Link_p >= Link_p_min_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) Link_ext_p_upper: description: "`Link-ext-p-upper` — an extendable link carries at most the chosen build" dims: [scenario, snapshot, link] where: Link_p_nom_extendable AND not Link_committable AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom_ext + expression: Link_p <= Link_p_max_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) Link_ext_p_nom_lower: description: "`Link-ext-p_nom-lower` — the chosen build is at least its floor" dims: [link] @@ -1804,22 +1999,22 @@ constraints: description: "`Process-fix-p-lower` — a fixed process runs at least its minimum, negative for the other way" dims: [scenario, snapshot, process] where: not Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom + expression: Process_p >= Process_p_min_pu * Process_p_nom * (1 - Process_maintenance_pu * Process_maintenance) Process_fix_p_upper: description: "`Process-fix-p-upper` — a fixed process runs at most its nominal power" dims: [scenario, snapshot, process] where: not Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom + expression: Process_p <= Process_p_max_pu * Process_p_nom * (1 - Process_maintenance_pu * Process_maintenance) Process_ext_p_lower: description: "`Process-ext-p-lower` — an extendable process runs at least its minimum of the chosen build, negative for the other way" dims: [scenario, snapshot, process] where: Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom_ext + expression: Process_p >= Process_p_min_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) Process_ext_p_upper: description: "`Process-ext-p-upper` — an extendable process runs at most the chosen build" dims: [scenario, snapshot, process] where: Process_p_nom_extendable AND not Process_committable AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom_ext + expression: Process_p <= Process_p_max_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) Process_ext_p_nom_lower: description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor" dims: [process] @@ -1871,12 +2066,12 @@ constraints: description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing" dims: [scenario, snapshot, generator] where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * Generator_status + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) Generator_com_p_upper: description: "`Generator-com-p-upper` — a committed unit outputs at most what is available; off, at most nothing" dims: [scenario, snapshot, generator] where: Generator_committable AND not Generator_p_nom_extendable AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * Generator_status + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) Generator_com_transition_start_up: description: "`Generator-com-transition-start-up` — turning on is a start, counted against the state the unit carried into the snapshot" dims: [scenario, snapshot, generator] @@ -1986,7 +2181,7 @@ constraints: at most what is available of the chosen build, whatever its status dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_ext + expression: Generator_p <= Generator_p_max_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) Generator_com_ext_p_upper_big_m: description: "`Generator-com-ext-p-upper-bigM` — off, a unit outputs nothing; on, the big M is no bound" dims: [scenario, snapshot, generator] @@ -2000,7 +2195,7 @@ constraints: where: Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND Generator_active expression: >- Generator_p >= - Generator_p_min_pu * Generator_p_nom_ext + Generator_p_min_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) + Generator_big_m * Generator_status - Generator_big_m Generator_com_ext_p_lower_nonneg: description: >- @@ -2018,14 +2213,14 @@ constraints: its minimum of one module, whether the build is fixed or a decision dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_mod * Generator_status + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom_mod * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) Generator_com_mod_p_upper: description: >- `Generator-com-mod-p-upper` — a committed modular unit outputs at most one module's share, whether the build is fixed or a decision dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_mod * Generator_status + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom_mod * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) Generator_status_p_fixed_upper: description: >- `Generator-status-p-fixed-upper` — a status is at most the modules in @@ -2065,16 +2260,88 @@ constraints: dims: [scenario, snapshot, generator] where: Generator_committable AND Generator_p_nom_extendable AND Generator_p_nom_mod > 0 AND Generator_active expression: Generator_shut_down <= Generator_n_mod + Generator_maint_event_count: + description: "`Generator-maint-event-count` — a maintainable generator holds its number of maintenance events over the horizon" + dims: [scenario, generator] + where: Generator_maintainable + expression: sum(Generator_maintenance_start, over=snapshot) == Generator_maintenance_events + Generator_maint_window: + description: >- + `Generator-maint-window` — a generator is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_active + expression: Generator_maintenance == sum(Generator_maintenance_start, by=Generator_maintenance_cover, over=start, into=covered) + Generator_maint_start_horizon: + description: "`Generator-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_active AND Generator_maintenance_start_blocked + expression: Generator_maintenance_start == 0 + Generator_maintcap_upper: + description: >- + `Generator-maintcap_upper` — the build taken off is at most the chosen build in + maintenance, and at most the build less its floor out of it + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active + expression: Generator_maintenance_capacity <= Generator_p_nom_ext - Generator_p_nom_min * (1 - Generator_maintenance) + Generator_maintcap_upper_nommax: + description: "`Generator-maintcap_upper_nommax` — out of maintenance, no build is taken off" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active + expression: Generator_maintenance_capacity <= Generator_p_nom_max * Generator_maintenance + Generator_maintcap_lower_nommax: + description: "`Generator-maintcap_lower_nommax` — in maintenance, the whole chosen build is taken off" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active + expression: Generator_maintenance_capacity >= Generator_p_nom_ext - Generator_p_nom_max * (1 - Generator_maintenance) + Generator_maintcap_lower_nommin: + description: "`Generator-maintcap_lower_nommin` — in maintenance, at least the floor of the build is taken off" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_p_nom_extendable AND NOT (Generator_committable AND Generator_p_nom_mod > 0) AND Generator_active AND Generator_p_nom_min > 0 + expression: Generator_maintenance_capacity >= Generator_p_nom_min * Generator_maintenance + Generator_maint_status_le_status: + description: "`Generator-maint-status-le-status` — the status in maintenance is at most the status" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_active + expression: Generator_maintenance_status <= Generator_status + Generator_maint_status_le_maint: + description: "`Generator-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_active + expression: Generator_maintenance_status <= Generator_maintenance + Generator_maint_status_lb: + description: "`Generator-maint-status-lb` — on and in maintenance, the status in maintenance is one" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_active + expression: Generator_maintenance_status >= Generator_status + Generator_maintenance - 1 + Generator_maint_modstatus_le_status: + description: "`Generator-maint-modstatus-le-status` — the modules on in maintenance are at most the modules on" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active + expression: Generator_maintenance_status <= Generator_status + Generator_maint_modstatus_le_maint: + description: >- + `Generator-maint-modstatus-le-maint` — out of maintenance, no module is on in + maintenance; in it, at most the modules the build cap holds + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active + expression: Generator_maintenance_status <= Generator_p_nom_max / Generator_p_nom_mod * Generator_maintenance + Generator_maint_modstatus_lb: + description: "`Generator-maint-modstatus-lb` — in maintenance, every module on is on in maintenance" + dims: [scenario, snapshot, generator] + where: Generator_maintainable AND Generator_committable AND Generator_p_nom_mod > 0 AND Generator_active + expression: Generator_maintenance_status >= Generator_status - Generator_p_nom_max / Generator_p_nom_mod * (1 - Generator_maintenance) Link_com_p_lower: description: "`Link-com-p-lower` — a committed link flows at least its minimum; off, at least nothing" dims: [scenario, snapshot, link] where: Link_committable AND not Link_p_nom_extendable AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom * Link_status + expression: Link_p >= Link_p_min_pu * Link_p_nom * (Link_status - Link_maintenance_pu * Link_maintenance_status) Link_com_p_upper: description: "`Link-com-p-upper` — a committed link flows at most what is available; off, at most nothing" dims: [scenario, snapshot, link] where: Link_committable AND not Link_p_nom_extendable AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom * Link_status + expression: Link_p <= Link_p_max_pu * Link_p_nom * (Link_status - Link_maintenance_pu * Link_maintenance_status) Link_com_transition_start_up: description: "`Link-com-transition-start-up` — turning on is a start, counted against the state the link carried into the snapshot" dims: [scenario, snapshot, link] @@ -2184,7 +2451,7 @@ constraints: at most what is available of the chosen build, whatever its status dims: [scenario, snapshot, link] where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom_ext + expression: Link_p <= Link_p_max_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) Link_com_ext_p_upper_big_m: description: "`Link-com-ext-p-upper-bigM` — off, a link flows nothing; on, the big M is no bound" dims: [scenario, snapshot, link] @@ -2198,7 +2465,7 @@ constraints: where: Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND Link_active expression: >- Link_p >= - Link_p_min_pu * Link_p_nom_ext + Link_p_min_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) + Link_big_m * Link_status - Link_big_m Link_com_ext_p_lower_nonneg: description: >- @@ -2216,14 +2483,14 @@ constraints: its minimum of one module, whether the build is fixed or a decision dims: [scenario, snapshot, link] where: Link_committable AND Link_p_nom_mod > 0 AND Link_active - expression: Link_p >= Link_p_min_pu * Link_p_nom_mod * Link_status + expression: Link_p >= Link_p_min_pu * Link_p_nom_mod * (Link_status - Link_maintenance_pu * Link_maintenance_status) Link_com_mod_p_upper: description: >- `Link-com-mod-p-upper` — a committed modular link flows at most one module's share, whether the build is fixed or a decision dims: [scenario, snapshot, link] where: Link_committable AND Link_p_nom_mod > 0 AND Link_active - expression: Link_p <= Link_p_max_pu * Link_p_nom_mod * Link_status + expression: Link_p <= Link_p_max_pu * Link_p_nom_mod * (Link_status - Link_maintenance_pu * Link_maintenance_status) Link_status_p_fixed_upper: description: >- `Link-status-p-fixed-upper` — a status is at most the modules in @@ -2263,16 +2530,88 @@ constraints: dims: [scenario, snapshot, link] where: Link_committable AND Link_p_nom_extendable AND Link_p_nom_mod > 0 AND Link_active expression: Link_shut_down <= Link_n_mod + Link_maint_event_count: + description: "`Link-maint-event-count` — a maintainable link holds its number of maintenance events over the horizon" + dims: [scenario, link] + where: Link_maintainable + expression: sum(Link_maintenance_start, over=snapshot) == Link_maintenance_events + Link_maint_window: + description: >- + `Link-maint-window` — a link is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_active + expression: Link_maintenance == sum(Link_maintenance_start, by=Link_maintenance_cover, over=start, into=covered) + Link_maint_start_horizon: + description: "`Link-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_active AND Link_maintenance_start_blocked + expression: Link_maintenance_start == 0 + Link_maintcap_upper: + description: >- + `Link-maintcap_upper` — the build taken off is at most the chosen build in + maintenance, and at most the build less its floor out of it + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_maintenance_capacity <= Link_p_nom_ext - Link_p_nom_min * (1 - Link_maintenance) + Link_maintcap_upper_nommax: + description: "`Link-maintcap_upper_nommax` — out of maintenance, no build is taken off" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_maintenance_capacity <= Link_p_nom_max * Link_maintenance + Link_maintcap_lower_nommax: + description: "`Link-maintcap_lower_nommax` — in maintenance, the whole chosen build is taken off" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active + expression: Link_maintenance_capacity >= Link_p_nom_ext - Link_p_nom_max * (1 - Link_maintenance) + Link_maintcap_lower_nommin: + description: "`Link-maintcap_lower_nommin` — in maintenance, at least the floor of the build is taken off" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_p_nom_extendable AND NOT (Link_committable AND Link_p_nom_mod > 0) AND Link_active AND Link_p_nom_min > 0 + expression: Link_maintenance_capacity >= Link_p_nom_min * Link_maintenance + Link_maint_status_le_status: + description: "`Link-maint-status-le-status` — the status in maintenance is at most the status" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_active + expression: Link_maintenance_status <= Link_status + Link_maint_status_le_maint: + description: "`Link-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_active + expression: Link_maintenance_status <= Link_maintenance + Link_maint_status_lb: + description: "`Link-maint-status-lb` — on and in maintenance, the status in maintenance is one" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_active + expression: Link_maintenance_status >= Link_status + Link_maintenance - 1 + Link_maint_modstatus_le_status: + description: "`Link-maint-modstatus-le-status` — the modules on in maintenance are at most the modules on" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_maintenance_status <= Link_status + Link_maint_modstatus_le_maint: + description: >- + `Link-maint-modstatus-le-maint` — out of maintenance, no module is on in + maintenance; in it, at most the modules the build cap holds + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_maintenance_status <= Link_p_nom_max / Link_p_nom_mod * Link_maintenance + Link_maint_modstatus_lb: + description: "`Link-maint-modstatus-lb` — in maintenance, every module on is on in maintenance" + dims: [scenario, snapshot, link] + where: Link_maintainable AND Link_committable AND Link_p_nom_mod > 0 AND Link_active + expression: Link_maintenance_status >= Link_status - Link_p_nom_max / Link_p_nom_mod * (1 - Link_maintenance) Process_com_p_lower: description: "`Process-com-p-lower` — a committed process runs at least its minimum; off, at least nothing" dims: [scenario, snapshot, process] where: Process_committable AND not Process_p_nom_extendable AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom * Process_status + expression: Process_p >= Process_p_min_pu * Process_p_nom * (Process_status - Process_maintenance_pu * Process_maintenance_status) Process_com_p_upper: description: "`Process-com-p-upper` — a committed process runs at most what is available; off, at most nothing" dims: [scenario, snapshot, process] where: Process_committable AND not Process_p_nom_extendable AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom * Process_status + expression: Process_p <= Process_p_max_pu * Process_p_nom * (Process_status - Process_maintenance_pu * Process_maintenance_status) Process_com_transition_start_up: description: "`Process-com-transition-start-up` — turning on is a start, counted against the state the process carried into the snapshot" dims: [scenario, snapshot, process] @@ -2382,7 +2721,7 @@ constraints: at most what is available of the chosen build, whatever its status dims: [scenario, snapshot, process] where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom_ext + expression: Process_p <= Process_p_max_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) Process_com_ext_p_upper_big_m: description: "`Process-com-ext-p-upper-bigM` — off, a process does not run; on, the big M is no bound" dims: [scenario, snapshot, process] @@ -2396,7 +2735,7 @@ constraints: where: Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND Process_active expression: >- Process_p >= - Process_p_min_pu * Process_p_nom_ext + Process_p_min_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) + Process_big_m * Process_status - Process_big_m Process_com_ext_p_lower_nonneg: description: >- @@ -2414,14 +2753,14 @@ constraints: its minimum of one module, whether the build is fixed or a decision dims: [scenario, snapshot, process] where: Process_committable AND Process_p_nom_mod > 0 AND Process_active - expression: Process_p >= Process_p_min_pu * Process_p_nom_mod * Process_status + expression: Process_p >= Process_p_min_pu * Process_p_nom_mod * (Process_status - Process_maintenance_pu * Process_maintenance_status) Process_com_mod_p_upper: description: >- `Process-com-mod-p-upper` — a committed modular process runs at most one module's share, whether the build is fixed or a decision dims: [scenario, snapshot, process] where: Process_committable AND Process_p_nom_mod > 0 AND Process_active - expression: Process_p <= Process_p_max_pu * Process_p_nom_mod * Process_status + expression: Process_p <= Process_p_max_pu * Process_p_nom_mod * (Process_status - Process_maintenance_pu * Process_maintenance_status) Process_status_p_fixed_upper: description: >- `Process-status-p-fixed-upper` — a status is at most the modules in @@ -2461,6 +2800,78 @@ constraints: dims: [scenario, snapshot, process] where: Process_committable AND Process_p_nom_extendable AND Process_p_nom_mod > 0 AND Process_active expression: Process_shut_down <= Process_n_mod + Process_maint_event_count: + description: "`Process-maint-event-count` — a maintainable process holds its number of maintenance events over the horizon" + dims: [scenario, process] + where: Process_maintainable + expression: sum(Process_maintenance_start, over=snapshot) == Process_maintenance_events + Process_maint_window: + description: >- + `Process-maint-window` — a process is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_active + expression: Process_maintenance == sum(Process_maintenance_start, by=Process_maintenance_cover, over=start, into=covered) + Process_maint_start_horizon: + description: "`Process-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_active AND Process_maintenance_start_blocked + expression: Process_maintenance_start == 0 + Process_maintcap_upper: + description: >- + `Process-maintcap_upper` — the build taken off is at most the chosen build in + maintenance, and at most the build less its floor out of it + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_maintenance_capacity <= Process_p_nom_ext - Process_p_nom_min * (1 - Process_maintenance) + Process_maintcap_upper_nommax: + description: "`Process-maintcap_upper_nommax` — out of maintenance, no build is taken off" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_maintenance_capacity <= Process_p_nom_max * Process_maintenance + Process_maintcap_lower_nommax: + description: "`Process-maintcap_lower_nommax` — in maintenance, the whole chosen build is taken off" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active + expression: Process_maintenance_capacity >= Process_p_nom_ext - Process_p_nom_max * (1 - Process_maintenance) + Process_maintcap_lower_nommin: + description: "`Process-maintcap_lower_nommin` — in maintenance, at least the floor of the build is taken off" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_p_nom_extendable AND NOT (Process_committable AND Process_p_nom_mod > 0) AND Process_active AND Process_p_nom_min > 0 + expression: Process_maintenance_capacity >= Process_p_nom_min * Process_maintenance + Process_maint_status_le_status: + description: "`Process-maint-status-le-status` — the status in maintenance is at most the status" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_active + expression: Process_maintenance_status <= Process_status + Process_maint_status_le_maint: + description: "`Process-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_active + expression: Process_maintenance_status <= Process_maintenance + Process_maint_status_lb: + description: "`Process-maint-status-lb` — on and in maintenance, the status in maintenance is one" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_active + expression: Process_maintenance_status >= Process_status + Process_maintenance - 1 + Process_maint_modstatus_le_status: + description: "`Process-maint-modstatus-le-status` — the modules on in maintenance are at most the modules on" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_maintenance_status <= Process_status + Process_maint_modstatus_le_maint: + description: >- + `Process-maint-modstatus-le-maint` — out of maintenance, no module is on in + maintenance; in it, at most the modules the build cap holds + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_maintenance_status <= Process_p_nom_max / Process_p_nom_mod * Process_maintenance + Process_maint_modstatus_lb: + description: "`Process-maint-modstatus-lb` — in maintenance, every module on is on in maintenance" + dims: [scenario, snapshot, process] + where: Process_maintainable AND Process_committable AND Process_p_nom_mod > 0 AND Process_active + expression: Process_maintenance_status >= Process_status - Process_p_nom_max / Process_p_nom_mod * (1 - Process_maintenance) Line_fix_s_lower: description: "`Line-fix-s-lower` — a fixed line carries at least the negative of its rating, the loss counted against it" dims: [scenario, snapshot, line] diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index e2db48d7..6c90cd23 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1184,5 +1184,214 @@ "Store-fix-e-lower": 8, "Store-fix-e-upper": 8 } + }, + "rung_33_maintenance": { + "columns": { + "Generator-maintenance": 8, + "Generator-maintenance_capacity": 4, + "Generator-maintenance_start": 8, + "Generator-p": 20, + "Generator-p_nom": 1, + "Link-maintenance": 8, + "Link-maintenance_capacity": 4, + "Link-maintenance_start": 8, + "Link-p": 12, + "Link-p_nom": 1, + "Process-maintenance": 8, + "Process-maintenance_capacity": 4, + "Process-maintenance_start": 8, + "Process-p": 8, + "Process-p_nom": 1 + }, + "global_constraints": {}, + "objective": 7367.560185185185, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-ext-p-lower": 4, + "Generator-ext-p-upper": 4, + "Generator-ext-p_nom-lower": 1, + "Generator-ext-p_nom-upper": 1, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Generator-maint-event-count": 2, + "Generator-maint-start-horizon": 1, + "Generator-maint-window": 8, + "Generator-maintcap_lower_nommax": 4, + "Generator-maintcap_lower_nommin": 4, + "Generator-maintcap_upper": 4, + "Generator-maintcap_upper_nommax": 4, + "Link-ext-p-lower": 4, + "Link-ext-p-upper": 4, + "Link-ext-p_nom-lower": 1, + "Link-ext-p_nom-upper": 1, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8, + "Link-maint-event-count": 2, + "Link-maint-start-horizon": 1, + "Link-maint-window": 8, + "Link-maintcap_lower_nommax": 4, + "Link-maintcap_lower_nommin": 4, + "Link-maintcap_upper": 4, + "Link-maintcap_upper_nommax": 4, + "Process-ext-p-lower": 4, + "Process-ext-p-upper": 4, + "Process-ext-p_nom-lower": 1, + "Process-ext-p_nom-upper": 1, + "Process-fix-p-lower": 4, + "Process-fix-p-upper": 4, + "Process-maint-event-count": 2, + "Process-maint-start-horizon": 1, + "Process-maint-window": 8, + "Process-maintcap_lower_nommax": 4, + "Process-maintcap_lower_nommin": 4, + "Process-maintcap_upper": 4, + "Process-maintcap_upper_nommax": 4 + } + }, + "rung_34_committable_maintenance": { + "columns": { + "Generator-maintenance": 12, + "Generator-maintenance_capacity": 4, + "Generator-maintenance_start": 12, + "Generator-maintenance_status": 8, + "Generator-n_mod": 1, + "Generator-p": 24, + "Generator-p_nom": 2, + "Generator-shut_down": 12, + "Generator-start_up": 12, + "Generator-status": 12, + "Link-maintenance": 12, + "Link-maintenance_capacity": 4, + "Link-maintenance_start": 12, + "Link-maintenance_status": 8, + "Link-n_mod": 1, + "Link-p": 16, + "Link-p_nom": 2, + "Link-shut_down": 12, + "Link-start_up": 12, + "Link-status": 12, + "Process-maintenance": 12, + "Process-maintenance_capacity": 4, + "Process-maintenance_start": 12, + "Process-maintenance_status": 8, + "Process-n_mod": 1, + "Process-p": 12, + "Process-p_nom": 2, + "Process-shut_down": 12, + "Process-start_up": 12, + "Process-status": 12 + }, + "global_constraints": {}, + "objective": 6420.048611111111, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-com-ext-p-lower": 4, + "Generator-com-ext-p-lower-nonneg": 4, + "Generator-com-ext-p-upper-bigM": 4, + "Generator-com-ext-p-upper-cap": 4, + "Generator-com-mod-p-lower": 4, + "Generator-com-mod-p-upper": 4, + "Generator-com-p-lower": 4, + "Generator-com-p-upper": 4, + "Generator-com-status-min_up_time_must_stay_up": 0, + "Generator-com-transition-shut-down": 12, + "Generator-com-transition-start-up": 12, + "Generator-ext-p_nom-lower": 2, + "Generator-ext-p_nom-upper": 2, + "Generator-fix-p-lower": 12, + "Generator-fix-p-upper": 12, + "Generator-maint-event-count": 3, + "Generator-maint-modstatus-lb": 4, + "Generator-maint-modstatus-le-maint": 4, + "Generator-maint-modstatus-le-status": 4, + "Generator-maint-start-horizon": 1, + "Generator-maint-status-lb": 4, + "Generator-maint-status-le-maint": 4, + "Generator-maint-status-le-status": 4, + "Generator-maint-window": 12, + "Generator-maintcap_lower_nommax": 4, + "Generator-maintcap_lower_nommin": 4, + "Generator-maintcap_upper": 4, + "Generator-maintcap_upper_nommax": 4, + "Generator-p_nom_modularity": 1, + "Generator-shut_down-p-fixed-upper": 8, + "Generator-shut_down-p_nom-variable-upper": 4, + "Generator-start_up-p-fixed-upper": 8, + "Generator-start_up-p_nom-variable-upper": 4, + "Generator-status-p-fixed-upper": 8, + "Generator-status-p_nom-variable-upper": 4, + "Link-com-ext-p-lower": 4, + "Link-com-ext-p-lower-nonneg": 4, + "Link-com-ext-p-upper-bigM": 4, + "Link-com-ext-p-upper-cap": 4, + "Link-com-mod-p-lower": 4, + "Link-com-mod-p-upper": 4, + "Link-com-p-lower": 4, + "Link-com-p-upper": 4, + "Link-com-status-min_up_time_must_stay_up": 0, + "Link-com-transition-shut-down": 12, + "Link-com-transition-start-up": 12, + "Link-ext-p_nom-lower": 2, + "Link-ext-p_nom-upper": 2, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Link-maint-event-count": 3, + "Link-maint-modstatus-lb": 4, + "Link-maint-modstatus-le-maint": 4, + "Link-maint-modstatus-le-status": 4, + "Link-maint-start-horizon": 1, + "Link-maint-status-lb": 4, + "Link-maint-status-le-maint": 4, + "Link-maint-status-le-status": 4, + "Link-maint-window": 12, + "Link-maintcap_lower_nommax": 4, + "Link-maintcap_lower_nommin": 4, + "Link-maintcap_upper": 4, + "Link-maintcap_upper_nommax": 4, + "Link-p_nom_modularity": 1, + "Link-shut_down-p-fixed-upper": 8, + "Link-shut_down-p_nom-variable-upper": 4, + "Link-start_up-p-fixed-upper": 8, + "Link-start_up-p_nom-variable-upper": 4, + "Link-status-p-fixed-upper": 8, + "Link-status-p_nom-variable-upper": 4, + "Process-com-ext-p-lower": 4, + "Process-com-ext-p-lower-nonneg": 4, + "Process-com-ext-p-upper-bigM": 4, + "Process-com-ext-p-upper-cap": 4, + "Process-com-mod-p-lower": 4, + "Process-com-mod-p-upper": 4, + "Process-com-p-lower": 4, + "Process-com-p-upper": 4, + "Process-com-status-min_up_time_must_stay_up": 0, + "Process-com-transition-shut-down": 12, + "Process-com-transition-start-up": 12, + "Process-ext-p_nom-lower": 2, + "Process-ext-p_nom-upper": 2, + "Process-maint-event-count": 3, + "Process-maint-modstatus-lb": 4, + "Process-maint-modstatus-le-maint": 4, + "Process-maint-modstatus-le-status": 4, + "Process-maint-start-horizon": 1, + "Process-maint-status-lb": 4, + "Process-maint-status-le-maint": 4, + "Process-maint-status-le-status": 4, + "Process-maint-window": 12, + "Process-maintcap_lower_nommax": 4, + "Process-maintcap_lower_nommin": 4, + "Process-maintcap_upper": 4, + "Process-maintcap_upper_nommax": 4, + "Process-p_nom_modularity": 1, + "Process-shut_down-p-fixed-upper": 8, + "Process-shut_down-p_nom-variable-upper": 4, + "Process-start_up-p-fixed-upper": 8, + "Process-start_up-p_nom-variable-upper": 4, + "Process-status-p-fixed-upper": 8, + "Process-status-p_nom-variable-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_33_maintenance.py b/examples/references/pypsa/rung_33_maintenance.py new file mode 100644 index 00000000..5a5ba805 --- /dev/null +++ b/examples/references/pypsa/rung_33_maintenance.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 33: maintenance — a fixed and an extendable generator, link and process, each taken off for events that the generator weightings time.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus and six maintainable components, fixed and extendable, with a dear peaker to cover them.""" + n = spine.build() + n.add('Bus', 'east') + n.add('Generator', 'hydro', bus='north', p_nom=50, marginal_cost=2, maintainable=True, maintenance_duration=3) + n.add( + 'Generator', + 'wind_ext', + bus='south', + p_nom_extendable=True, + p_nom_min=10, + p_nom_max=60, + capital_cost=20, + marginal_cost=1, + p_max_pu=[0.9, 0.6, 0.8, 0.7], + maintainable=True, + maintenance_duration=2, + maintenance_pu=0.5, + ) + n.add( + 'Link', + 'tie_fix', + bus0='north', + bus1='east', + p_nom=30, + efficiency=0.95, + maintainable=True, + maintenance_duration=1, + maintenance_events=2, + ) + n.add( + 'Link', + 'tie_ext', + bus0='south', + bus1='east', + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=40, + capital_cost=4, + efficiency=0.9, + p_min_pu=-1, + maintainable=True, + maintenance_duration=3, + ) + n.add( + 'Process', + 'conv_fix', + bus0='north', + bus1='east', + rate0=-1.25, + p_nom=25, + maintainable=True, + maintenance_duration=3, + maintenance_pu=0.6, + ) + n.add( + 'Process', + 'conv_ext', + bus0='south', + bus1='east', + rate0=-1.25, + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=30, + capital_cost=3, + maintainable=True, + maintenance_duration=1, + ) + n.add('Generator', 'east_peak', bus='east', p_nom=100, marginal_cost=60) + n.add('Load', 'east_load', bus='east', p_set=[50, 60, 40, 55]) + return n diff --git a/examples/references/pypsa/rung_34_committable_maintenance.py b/examples/references/pypsa/rung_34_committable_maintenance.py new file mode 100644 index 00000000..f78b57d1 --- /dev/null +++ b/examples/references/pypsa/rung_34_committable_maintenance.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 34: maintenance of committable units — a fixed, an extendable and a modular committable generator, link and process, each taken off for one event.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus an east bus and nine maintainable committable components, with a dear peaker to cover them.""" + n = spine.build() + n.add('Bus', 'east') + committable = {'committable': True, 'maintainable': True} + n.add( + 'Generator', + 'unit', + bus='north', + p_nom=60, + p_min_pu=0.3, + marginal_cost=5, + start_up_cost=10, + maintenance_duration=2, + **committable, + ) + n.add( + 'Generator', + 'unit_ext', + bus='south', + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=50, + capital_cost=10, + p_min_pu=0.2, + marginal_cost=4, + maintenance_duration=3, + maintenance_pu=0.5, + **committable, + ) + n.add( + 'Generator', + 'unit_mod', + bus='south', + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=30, + capital_cost=8, + p_min_pu=0.5, + marginal_cost=3, + maintenance_duration=1, + **committable, + ) + for component, ports in (('Link', {}), ('Process', {'rate0': -1.25})): + n.add( + component, + f'{component.lower()}_unit', + bus0='north', + bus1='east', + p_nom=40, + p_min_pu=0.2, + marginal_cost=1, + maintenance_duration=2, + **ports, + **committable, + ) + n.add( + component, + f'{component.lower()}_ext', + bus0='south', + bus1='east', + p_nom_extendable=True, + p_nom_min=5, + p_nom_max=30, + capital_cost=5, + p_min_pu=0.2, + maintenance_duration=3, + maintenance_pu=0.5, + **ports, + **committable, + ) + n.add( + component, + f'{component.lower()}_mod', + bus0='north', + bus1='east', + p_nom_extendable=True, + p_nom_mod=10, + p_nom_max=20, + capital_cost=3, + p_min_pu=0.5, + maintenance_duration=1, + **ports, + **committable, + ) + n.add('Generator', 'east_peak', bus='east', p_nom=100, marginal_cost=60) + n.add('Load', 'east_load', bus='east', p_set=[50, 60, 40, 55]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index d7ecaa05..eeb940f5 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -308,3 +308,27 @@ names: Transformer_first_active: '\mathrm{new}^{\sigma}' Carrier_max_growth: '\overline{\Delta}' Carrier_max_relative_growth: '\mathrm{r}' + Generator_maintainable: '\mathrm{mnt}' + Link_maintainable: '\mathrm{mnt}^{f}' + Process_maintainable: '\mathrm{mnt}^{z}' + Generator_maintenance_pu: '\gamma' + Link_maintenance_pu: '\gamma^{f}' + Process_maintenance_pu: '\gamma^{z}' + Generator_maintenance_events: '\mathrm{n}^{\mathrm{mnt}}' + Link_maintenance_events: '\mathrm{n}^{f,\mathrm{mnt}}' + Process_maintenance_events: '\mathrm{n}^{z,\mathrm{mnt}}' + Generator_maintenance_start_blocked: '\mathrm{blk}' + Link_maintenance_start_blocked: '\mathrm{blk}^{f}' + Process_maintenance_start_blocked: '\mathrm{blk}^{z}' + Generator_maintenance: '\mu' + Link_maintenance: '\mu^{f}' + Process_maintenance: '\mu^{z}' + Generator_maintenance_start: '\mu^{\mathrm{up}}' + Link_maintenance_start: '\mu^{f,\mathrm{up}}' + Process_maintenance_start: '\mu^{z,\mathrm{up}}' + Generator_maintenance_capacity: '\mu^{\mathrm{nom}}' + Link_maintenance_capacity: '\mu^{f,\mathrm{nom}}' + Process_maintenance_capacity: '\mu^{z,\mathrm{nom}}' + Generator_maintenance_status: '\mu^{u}' + Link_maintenance_status: '\mu^{f,u}' + Process_maintenance_status: '\mu^{z,u}' From eec71d2bbefe88f82584db2cf350eb63330c6847 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 13:02:36 +0200 Subject: [PATCH 20/46] docs(pypsa): the spec states what maintenance and late-opening storage assume of the data --- docs/examples/pypsa.md | 400 +++++++++++++++++++++++++++++++++++- examples/pypsa.yaml | 189 +++++++++++++++++ examples/symbols/pypsa.yaml | 3 + 3 files changed, 587 insertions(+), 5 deletions(-) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 9988d174..454dd7c3 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -2566,7 +2566,14 @@ horizon, the file states the same rows with two data-prep parameters. The opening snapshot past the horizon's first is `{c}_opens_late`. The number of snapshots the storage does not stand in is `{c}_inactive_snapshots`, and a cyclic storage reaches back that many snapshots further. A plain run feeds -false and zero, so the rows collapse to the standard ones. +false and zero, so the rows collapse to the standard ones. The assumptions +[`StorageUnit_stands_in_one_run`](#storageunit_stands_in_one_run), +[`-opens_late_only_where_it_opens`](#storageunit_opens_late_only_where_it_opens) +and [`-opens_late_where_it_opens`](#storageunit_opens_late_where_it_opens), and +the `Store` ones, state the run and the opening snapshot. No +assumption ties `{c}_inactive_snapshots` to the count of inactive snapshots, +because a `count` compares against a whole number and not against a +parameter. The rung builds a cyclic storage unit and a store with an initial level of 5 in 2030, and a cyclic store that retires after 2020. The earlier file read the @@ -2907,8 +2914,9 @@ def build(): ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language -gap; each is a data check not made yet, and where it should live — language, -data prep, or harness — is one open question. Line numbers are pinned pypsa +gap. The maintenance checks are assumptions of the file, which the consumer +that binds the data runs. Each other one is a data check not made yet, and +where it should live — language, data prep, or harness — is one open question. Line numbers are pinned pypsa 1.3.0, the version the records above are from. | PyPSA raises | on | here | note | @@ -2920,8 +2928,8 @@ data prep, or harness — is one open question. Line numbers are pinned pypsa | `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` and `Transformer_loss_max` | X4 | | `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | | `ValueError`, `abstract.py:427`, `:445` | a security-constrained run over scenarios | rows per scenario, not refused | | -| `ConsistencyError`, `consistency.py:1506-1560` | a maintainable component whose `maintenance_duration` or `maintenance_events` is not positive, whose events do not fit the weighted horizon, or that is extendable with `p_nom_max = inf` | data prep, at `*_maintenance_cover` and `*_maintenance_start_blocked`; an infinite coefficient in the `maintcap` rows | | -| nothing; HiGHS refuses the model, `constraints.py:500-503` | a fixed modular committable maintainable build, whose module count `p_nom_max / p_nom_mod` is infinite | an infinite coefficient in the `maint-modstatus` rows | | +| `ConsistencyError`, `consistency.py:1506-1560` | a maintainable component whose `maintenance_duration` or `maintenance_events` is not positive, whose events do not fit the weighted horizon, or that is extendable with `p_nom_max = inf` | assumed: [`Generator_maintenance_events_positive`](#generator_maintenance_events_positive), [`-duration_positive`](#generator_maintenance_duration_positive), [`-duration_fits_the_horizon`](#generator_maintenance_duration_fits_the_horizon), [`-events_fit_the_horizon`](#generator_maintenance_events_fit_the_horizon), [`-build_cap_is_finite`](#generator_maintenance_build_cap_is_finite), and the `Link` and `Process` ones | | +| nothing; HiGHS refuses the model, `constraints.py:500-503` | a fixed modular committable maintainable build, whose module count `p_nom_max / p_nom_mod` is infinite | assumed: [`Generator_maintenance_module_count_is_finite`](#generator_maintenance_module_count_is_finite), and the `Link` and `Process` ones | | Duals and solutions are read back by the harness on the lpspec side: `marginal_price` is the balance dual over `w_objective`, `mu_upper` the @@ -2987,6 +2995,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{mnt}`$ | `Generator_maintainable` over $`\mathcal{G}`$ — whether a generator must be taken off for maintenance within the horizon | | $`\gamma`$ | `Generator_maintenance_pu` over $`\mathcal{G}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\mathcal{G}`$ — how many maintenance events the horizon holds | +| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\mathcal{G}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit | | $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit | @@ -3017,6 +3026,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{mnt}^{f}`$ | `Link_maintainable` over $`\mathcal{L}`$ — whether a link must be taken off for maintenance within the horizon | | $`\gamma^{f}`$ | `Link_maintenance_pu` over $`\mathcal{L}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{f,\mathrm{mnt}}`$ | `Link_maintenance_events` over $`\mathcal{L}`$ — how many maintenance events the horizon holds | +| $`\tau^{f,\mathrm{mnt}}`$ | `Link_maintenance_duration` over $`\mathcal{L}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the link is not maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}^{f}`$ | `Link_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{L}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the link does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\mathcal{J}`$ — nominal internal power | | $`\mathrm{ext}^{z}`$ | `Process_p_nom_extendable` over $`\mathcal{J}`$ — whether the nominal internal power is a decision | @@ -3051,6 +3061,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{mnt}^{z}`$ | `Process_maintainable` over $`\mathcal{J}`$ — whether a process must be taken off for maintenance within the horizon | | $`\gamma^{z}`$ | `Process_maintenance_pu` over $`\mathcal{J}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{z,\mathrm{mnt}}`$ | `Process_maintenance_events` over $`\mathcal{J}`$ — how many maintenance events the horizon holds | +| $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\mathcal{J}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | @@ -8443,6 +8454,385 @@ a_{\xi} \ge 0 \qquad \forall\, \xi \in \Xi ```math CVaR \in \mathbb{R} ``` + +### `Generator_maintenance_events_positive` + +```yaml +Generator_maintenance_events_positive: + holds: "Generator_maintenance_events > 0" + where: "Generator_maintainable" + description: >- + a maintainable generator with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) +``` + +```math +\mathrm{n}^{\mathrm{mnt}}_{g} > 0 \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator_maintenance_duration_positive` + +```yaml +Generator_maintenance_duration_positive: + holds: "Generator_maintenance_duration > 0" + where: "Generator_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) +``` + +```math +\tau^{\mathrm{mnt}}_{g} > 0 \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator_maintenance_duration_fits_the_horizon` + +```yaml +Generator_maintenance_duration_fits_the_horizon: + holds: "Generator_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) +``` + +```math +\tau^{\mathrm{mnt}}_{g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator_maintenance_events_fit_the_horizon` + +```yaml +Generator_maintenance_events_fit_the_horizon: + holds: "Generator_maintenance_duration * Generator_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) +``` + +```math +\tau^{\mathrm{mnt}}_{g} \cdot \mathrm{n}^{\mathrm{mnt}}_{g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator_maintenance_build_cap_is_finite` + +```yaml +Generator_maintenance_build_cap_is_finite: + holds: "Generator_p_nom_max < inf" + where: "Generator_maintainable AND Generator_p_nom_extendable" + description: >- + the `maintcap` rows hold the chosen build in maintenance against + `p_nom_max`, so an infinite cap is an infinite coefficient — PyPSA + refuses it (`consistency.py:1551`) +``` + +```math +\overline{\mathrm{p}}^{\mathrm{nom}}_{g} < \infty \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} +``` + +### `Generator_maintenance_module_count_is_finite` + +```yaml +Generator_maintenance_module_count_is_finite: + holds: "Generator_p_nom_max < inf" + where: "Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_p_nom_mod > 0" + description: >- + the `maint-modstatus` rows bound the modules on in maintenance by + `p_nom_max / p_nom_mod`, so an infinite cap is an infinite + coefficient. PyPSA does not check it, and HiGHS refuses the model + (`constraints.py:500-503`) +``` + +```math +\overline{\mathrm{p}}^{\mathrm{nom}}_{g} < \infty \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 +``` + +### `Link_maintenance_events_positive` + +```yaml +Link_maintenance_events_positive: + holds: "Link_maintenance_events > 0" + where: "Link_maintainable" + description: >- + a maintainable link with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) +``` + +```math +\mathrm{n}^{f,\mathrm{mnt}}_{l} > 0 \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +``` + +### `Link_maintenance_duration_positive` + +```yaml +Link_maintenance_duration_positive: + holds: "Link_maintenance_duration > 0" + where: "Link_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) +``` + +```math +\tau^{f,\mathrm{mnt}}_{l} > 0 \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +``` + +### `Link_maintenance_duration_fits_the_horizon` + +```yaml +Link_maintenance_duration_fits_the_horizon: + holds: "Link_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Link_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) +``` + +```math +\tau^{f,\mathrm{mnt}}_{l} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +``` + +### `Link_maintenance_events_fit_the_horizon` + +```yaml +Link_maintenance_events_fit_the_horizon: + holds: "Link_maintenance_duration * Link_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Link_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) +``` + +```math +\tau^{f,\mathrm{mnt}}_{l} \cdot \mathrm{n}^{f,\mathrm{mnt}}_{l} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +``` + +### `Link_maintenance_build_cap_is_finite` + +```yaml +Link_maintenance_build_cap_is_finite: + holds: "Link_p_nom_max < inf" + where: "Link_maintainable AND Link_p_nom_extendable" + description: >- + the `maintcap` rows hold the chosen build in maintenance against + `p_nom_max`, so an infinite cap is an infinite coefficient — PyPSA + refuses it (`consistency.py:1551`) +``` + +```math +\overline{\mathrm{f}}^{\mathrm{nom}}_{l} < \infty \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} +``` + +### `Link_maintenance_module_count_is_finite` + +```yaml +Link_maintenance_module_count_is_finite: + holds: "Link_p_nom_max < inf" + where: "Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_p_nom_mod > 0" + description: >- + the `maint-modstatus` rows bound the modules on in maintenance by + `p_nom_max / p_nom_mod`, so an infinite cap is an infinite + coefficient. PyPSA does not check it, and HiGHS refuses the model + (`constraints.py:500-503`) +``` + +```math +\overline{\mathrm{f}}^{\mathrm{nom}}_{l} < \infty \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 +``` + +### `Process_maintenance_events_positive` + +```yaml +Process_maintenance_events_positive: + holds: "Process_maintenance_events > 0" + where: "Process_maintainable" + description: >- + a maintainable process with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) +``` + +```math +\mathrm{n}^{z,\mathrm{mnt}}_{j} > 0 \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +``` + +### `Process_maintenance_duration_positive` + +```yaml +Process_maintenance_duration_positive: + holds: "Process_maintenance_duration > 0" + where: "Process_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) +``` + +```math +\tau^{z,\mathrm{mnt}}_{j} > 0 \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +``` + +### `Process_maintenance_duration_fits_the_horizon` + +```yaml +Process_maintenance_duration_fits_the_horizon: + holds: "Process_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Process_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) +``` + +```math +\tau^{z,\mathrm{mnt}}_{j} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +``` + +### `Process_maintenance_events_fit_the_horizon` + +```yaml +Process_maintenance_events_fit_the_horizon: + holds: "Process_maintenance_duration * Process_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Process_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) +``` + +```math +\tau^{z,\mathrm{mnt}}_{j} \cdot \mathrm{n}^{z,\mathrm{mnt}}_{j} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +``` + +### `Process_maintenance_build_cap_is_finite` + +```yaml +Process_maintenance_build_cap_is_finite: + holds: "Process_p_nom_max < inf" + where: "Process_maintainable AND Process_p_nom_extendable" + description: >- + the `maintcap` rows hold the chosen build in maintenance against + `p_nom_max`, so an infinite cap is an infinite coefficient — PyPSA + refuses it (`consistency.py:1551`) +``` + +```math +\overline{\mathrm{z}}^{\mathrm{nom}}_{j} < \infty \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} +``` + +### `Process_maintenance_module_count_is_finite` + +```yaml +Process_maintenance_module_count_is_finite: + holds: "Process_p_nom_max < inf" + where: "Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_p_nom_mod > 0" + description: >- + the `maint-modstatus` rows bound the modules on in maintenance by + `p_nom_max / p_nom_mod`, so an infinite cap is an infinite + coefficient. PyPSA does not check it, and HiGHS refuses the model + (`constraints.py:500-503`) +``` + +```math +\overline{\mathrm{z}}^{\mathrm{nom}}_{j} < \infty \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 +``` + +### `StorageUnit_stands_in_one_run` + +```yaml +StorageUnit_stands_in_one_run: + holds: "count(StorageUnit_active AND NOT shift(StorageUnit_active, along=snapshot, offset=1), over=snapshot) <= 1" + description: >- + the snapshots a storage unit stands in are one unbroken run, as a build + year and a lifetime make them. The opening row holds at the first + of them only, and a cyclic storage unit reaches back + `StorageUnit_inactive_snapshots` further to the last of them +``` + +```math +\lvert \{ t \in \mathcal{T} \,:\, \mathrm{on}^{h}_{t,s} \wedge \neg \mathrm{on}^{h}_{t - 1,s} \} \rvert \le 1 \qquad \forall\, s \in \mathcal{S} +``` + +### `StorageUnit_opens_late_only_where_it_opens` + +```yaml +StorageUnit_opens_late_only_where_it_opens: + holds: "StorageUnit_active AND NOT shift(StorageUnit_active, along=snapshot, offset=1) AND position(snapshot) > 0" + where: "StorageUnit_opens_late" + description: >- + `StorageUnit_opens_late` marks the first snapshot the storage unit stands in, past + the first of the horizon, and no other +``` + +```math +\mathrm{on}^{h}_{t,s} \wedge \neg \mathrm{on}^{h}_{t - 1,s} \wedge \mathrm{pos}(t) > 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{open}_{t,s} +``` + +### `StorageUnit_opens_late_where_it_opens` + +```yaml +StorageUnit_opens_late_where_it_opens: + holds: "StorageUnit_opens_late" + where: "StorageUnit_active AND NOT shift(StorageUnit_active, along=snapshot, offset=1) AND position(snapshot) > 0" + description: >- + a storage unit that opens past the first snapshot of the horizon opens on + its initial level, or its last level where it is cyclic, only where + `StorageUnit_opens_late` marks the snapshot +``` + +```math +\mathrm{open}_{t,s} \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{on}^{h}_{t,s} \wedge \neg \mathrm{on}^{h}_{t - 1,s} \wedge \mathrm{pos}(t) > 0 +``` + +### `Store_stands_in_one_run` + +```yaml +Store_stands_in_one_run: + holds: "count(Store_active AND NOT shift(Store_active, along=snapshot, offset=1), over=snapshot) <= 1" + description: >- + the snapshots a store stands in are one unbroken run, as a build + year and a lifetime make them. The opening row holds at the first + of them only, and a cyclic store reaches back + `Store_inactive_snapshots` further to the last of them +``` + +```math +\lvert \{ t \in \mathcal{T} \,:\, \mathrm{on}^{e}_{t,v} \wedge \neg \mathrm{on}^{e}_{t - 1,v} \} \rvert \le 1 \qquad \forall\, v \in \mathcal{V} +``` + +### `Store_opens_late_only_where_it_opens` + +```yaml +Store_opens_late_only_where_it_opens: + holds: "Store_active AND NOT shift(Store_active, along=snapshot, offset=1) AND position(snapshot) > 0" + where: "Store_opens_late" + description: >- + `Store_opens_late` marks the first snapshot the store stands in, past + the first of the horizon, and no other +``` + +```math +\mathrm{on}^{e}_{t,v} \wedge \neg \mathrm{on}^{e}_{t - 1,v} \wedge \mathrm{pos}(t) > 0 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{open}^{e}_{t,v} +``` + +### `Store_opens_late_where_it_opens` + +```yaml +Store_opens_late_where_it_opens: + holds: "Store_opens_late" + where: "Store_active AND NOT shift(Store_active, along=snapshot, offset=1) AND position(snapshot) > 0" + description: >- + a store that opens past the first snapshot of the horizon opens on + its initial level, or its last level where it is cyclic, only where + `Store_opens_late` marks the snapshot +``` + +```math +\mathrm{open}^{e}_{t,v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} \wedge \neg \mathrm{on}^{e}_{t - 1,v} \wedge \mathrm{pos}(t) > 0 +``` Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index a400d145..a56f8a86 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -174,6 +174,14 @@ parameters: description: how many maintenance events the horizon holds dims: [generator] dtype: int + Generator_maintenance_duration: + description: >- + the hours of generator weightings one maintenance event covers — + PyPSA's `maintenance_duration`; no value where the generator is not + maintainable. No row reads it: data prep turns it into + `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the + assumptions hold it to the horizon + dims: [generator] Generator_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -302,6 +310,14 @@ parameters: description: how many maintenance events the horizon holds dims: [link] dtype: int + Link_maintenance_duration: + description: >- + the hours of generator weightings one maintenance event covers — + PyPSA's `maintenance_duration`; no value where the link is not + maintainable. No row reads it: data prep turns it into + `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the + assumptions hold it to the horizon + dims: [link] Link_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -444,6 +460,14 @@ parameters: description: how many maintenance events the horizon holds dims: [process] dtype: int + Process_maintenance_duration: + description: >- + the hours of generator weightings one maintenance event covers — + PyPSA's `maintenance_duration`; no value where the process is not + maintainable. No row reads it: data prep turns it into + `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the + assumptions hold it to the horizon + dims: [process] Process_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -3434,6 +3458,171 @@ constraints: dims: [] expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR +assumptions: + Generator_maintenance_events_positive: + holds: "Generator_maintenance_events > 0" + where: "Generator_maintainable" + description: >- + a maintainable generator with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) + Generator_maintenance_duration_positive: + holds: "Generator_maintenance_duration > 0" + where: "Generator_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) + Generator_maintenance_duration_fits_the_horizon: + holds: "Generator_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) + Generator_maintenance_events_fit_the_horizon: + holds: "Generator_maintenance_duration * Generator_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) + Generator_maintenance_build_cap_is_finite: + holds: "Generator_p_nom_max < inf" + where: "Generator_maintainable AND Generator_p_nom_extendable" + description: >- + the `maintcap` rows hold the chosen build in maintenance against + `p_nom_max`, so an infinite cap is an infinite coefficient — PyPSA + refuses it (`consistency.py:1551`) + Generator_maintenance_module_count_is_finite: + holds: "Generator_p_nom_max < inf" + where: "Generator_maintainable AND Generator_committable AND NOT Generator_p_nom_extendable AND Generator_p_nom_mod > 0" + description: >- + the `maint-modstatus` rows bound the modules on in maintenance by + `p_nom_max / p_nom_mod`, so an infinite cap is an infinite + coefficient. PyPSA does not check it, and HiGHS refuses the model + (`constraints.py:500-503`) + Link_maintenance_events_positive: + holds: "Link_maintenance_events > 0" + where: "Link_maintainable" + description: >- + a maintainable link with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) + Link_maintenance_duration_positive: + holds: "Link_maintenance_duration > 0" + where: "Link_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) + Link_maintenance_duration_fits_the_horizon: + holds: "Link_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Link_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) + Link_maintenance_events_fit_the_horizon: + holds: "Link_maintenance_duration * Link_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Link_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) + Link_maintenance_build_cap_is_finite: + holds: "Link_p_nom_max < inf" + where: "Link_maintainable AND Link_p_nom_extendable" + description: >- + the `maintcap` rows hold the chosen build in maintenance against + `p_nom_max`, so an infinite cap is an infinite coefficient — PyPSA + refuses it (`consistency.py:1551`) + Link_maintenance_module_count_is_finite: + holds: "Link_p_nom_max < inf" + where: "Link_maintainable AND Link_committable AND NOT Link_p_nom_extendable AND Link_p_nom_mod > 0" + description: >- + the `maint-modstatus` rows bound the modules on in maintenance by + `p_nom_max / p_nom_mod`, so an infinite cap is an infinite + coefficient. PyPSA does not check it, and HiGHS refuses the model + (`constraints.py:500-503`) + Process_maintenance_events_positive: + holds: "Process_maintenance_events > 0" + where: "Process_maintainable" + description: >- + a maintainable process with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) + Process_maintenance_duration_positive: + holds: "Process_maintenance_duration > 0" + where: "Process_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) + Process_maintenance_duration_fits_the_horizon: + holds: "Process_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Process_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) + Process_maintenance_events_fit_the_horizon: + holds: "Process_maintenance_duration * Process_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Process_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) + Process_maintenance_build_cap_is_finite: + holds: "Process_p_nom_max < inf" + where: "Process_maintainable AND Process_p_nom_extendable" + description: >- + the `maintcap` rows hold the chosen build in maintenance against + `p_nom_max`, so an infinite cap is an infinite coefficient — PyPSA + refuses it (`consistency.py:1551`) + Process_maintenance_module_count_is_finite: + holds: "Process_p_nom_max < inf" + where: "Process_maintainable AND Process_committable AND NOT Process_p_nom_extendable AND Process_p_nom_mod > 0" + description: >- + the `maint-modstatus` rows bound the modules on in maintenance by + `p_nom_max / p_nom_mod`, so an infinite cap is an infinite + coefficient. PyPSA does not check it, and HiGHS refuses the model + (`constraints.py:500-503`) + StorageUnit_stands_in_one_run: + holds: "count(StorageUnit_active AND NOT shift(StorageUnit_active, along=snapshot, offset=1), over=snapshot) <= 1" + description: >- + the snapshots a storage unit stands in are one unbroken run, as a build + year and a lifetime make them. The opening row holds at the first + of them only, and a cyclic storage unit reaches back + `StorageUnit_inactive_snapshots` further to the last of them + StorageUnit_opens_late_only_where_it_opens: + holds: "StorageUnit_active AND NOT shift(StorageUnit_active, along=snapshot, offset=1) AND position(snapshot) > 0" + where: "StorageUnit_opens_late" + description: >- + `StorageUnit_opens_late` marks the first snapshot the storage unit stands in, past + the first of the horizon, and no other + StorageUnit_opens_late_where_it_opens: + holds: "StorageUnit_opens_late" + where: "StorageUnit_active AND NOT shift(StorageUnit_active, along=snapshot, offset=1) AND position(snapshot) > 0" + description: >- + a storage unit that opens past the first snapshot of the horizon opens on + its initial level, or its last level where it is cyclic, only where + `StorageUnit_opens_late` marks the snapshot + Store_stands_in_one_run: + holds: "count(Store_active AND NOT shift(Store_active, along=snapshot, offset=1), over=snapshot) <= 1" + description: >- + the snapshots a store stands in are one unbroken run, as a build + year and a lifetime make them. The opening row holds at the first + of them only, and a cyclic store reaches back + `Store_inactive_snapshots` further to the last of them + Store_opens_late_only_where_it_opens: + holds: "Store_active AND NOT shift(Store_active, along=snapshot, offset=1) AND position(snapshot) > 0" + where: "Store_opens_late" + description: >- + `Store_opens_late` marks the first snapshot the store stands in, past + the first of the horizon, and no other + Store_opens_late_where_it_opens: + holds: "Store_opens_late" + where: "Store_active AND NOT shift(Store_active, along=snapshot, offset=1) AND position(snapshot) > 0" + description: >- + a store that opens past the first snapshot of the horizon opens on + its initial level, or its last level where it is cyclic, only where + `Store_opens_late` marks the snapshot + objective: sense: minimize description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index eeb940f5..299b8d13 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -317,6 +317,9 @@ names: Generator_maintenance_events: '\mathrm{n}^{\mathrm{mnt}}' Link_maintenance_events: '\mathrm{n}^{f,\mathrm{mnt}}' Process_maintenance_events: '\mathrm{n}^{z,\mathrm{mnt}}' + Generator_maintenance_duration: '\tau^{\mathrm{mnt}}' + Link_maintenance_duration: '\tau^{f,\mathrm{mnt}}' + Process_maintenance_duration: '\tau^{z,\mathrm{mnt}}' Generator_maintenance_start_blocked: '\mathrm{blk}' Link_maintenance_start_blocked: '\mathrm{blk}^{f}' Process_maintenance_start_blocked: '\mathrm{blk}^{z}' From 64b50d010f825b58155392f4070430cf05fe62cb Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 13:28:38 +0200 Subject: [PATCH 21/46] docs(pypsa): a growth limit counts no transformer, as pypsa does --- docs/examples/pypsa.md | 16 +++++++--------- examples/pypsa.yaml | 14 +++----------- examples/symbols/pypsa.yaml | 1 - 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 454dd7c3..db1ce623 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -2956,13 +2956,13 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | | $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | | $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K}`$ — passive branches, each between two buses, their flow set by impedance | -| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep | | $`\mathcal{K}^{\mathrm{out}}`$ | index $`\kappa`$ — `outage` with $`\mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$ — the passive branches a security-constrained run takes out one at a time — PyPSA's `branch_outages`, each a line or a transformer; none on a plain run | | $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a passive branch's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | | $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Transformer\_carrier}: \mathcal{M} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -3088,7 +3088,6 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{new}^{e}`$ | `Store_first_active` over $`\mathcal{Y} \times \mathcal{V}`$ — one in the first period a store stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | | $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | | $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\mathrm{new}^{\sigma}`$ | `Transformer_first_active` over $`\mathcal{Y} \times \mathcal{M}`$ — one in the first period a transformer stands in, zero elsewhere — the spec extends the carrier growth limit to transformers, which PyPSA does not, so PyPSA has no counterpart, data prep | | $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit | | $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top | | $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | @@ -3297,7 +3296,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\mathcal{I}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | -| $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. PyPSA sums the components that carry a carrier attribute; the transformer term is the spec's own extension, since PyPSA gives a transformer no carrier | +| $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. Like PyPSA, it sums only the components that carry a carrier attribute, so a transformer, which has none, counts in no carrier | | $`\check{s}`$ | `Line_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the flow a line's post-contingency rows read — its flow where it stands, nothing where it does not, since PyPSA builds those rows for every branch of the sub-network in every snapshot | | $`\check{\sigma}`$ | `Transformer_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — the flow a transformer's post-contingency rows read, as a line's | | $`\hat{s}`$ | `Outage_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}^{\mathrm{out}}`$ — the flow an outage takes off its branch — the outaged line's or transformer's flow before it goes out | @@ -8098,9 +8097,9 @@ scenario_opex: Carrier_additions: description: >- what a carrier adds in a period — every extendable component of that - carrier, counting each build in the first period it stands in. PyPSA sums - the components that carry a carrier attribute; the transformer term is the - spec's own extension, since PyPSA gives a transformer no carrier + carrier, counting each build in the first period it stands in. Like PyPSA, + it sums only the components that carry a carrier attribute, so a + transformer, which has none, counts in no carrier expression: >- sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) + sum(Link_p_nom_ext * Link_first_active, by=Link_carrier, over=link, into=carrier) @@ -8108,11 +8107,10 @@ Carrier_additions: + sum(Store_e_nom_ext * Store_first_active, by=Store_carrier, over=store, into=carrier) + sum(Line_s_nom_ext * Line_first_active, by=Line_carrier, over=line, into=carrier) + sum(Process_p_nom_ext * Process_first_active, by=Process_carrier, over=process, into=carrier) - + sum(Transformer_s_nom_ext * Transformer_first_active, by=Transformer_carrier, over=transformer, into=carrier) ``` ```math -\mathit{Carrier\_additions}_{y,i} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} + \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} + \sum_{j \in \mathcal{J} \,:\, \mathrm{Process\_carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_carrier}(m) = i} \Sigma_{m} \cdot \mathrm{new}^{\sigma}_{y,m} \qquad \forall\, y \in \mathcal{Y},\ i \in \mathcal{I} +\mathit{Carrier\_additions}_{y,i} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} + \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} + \sum_{j \in \mathcal{J} \,:\, \mathrm{Process\_carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} \qquad \forall\, y \in \mathcal{Y},\ i \in \mathcal{I} ``` ### `Line_s_monitored` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index a56f8a86..5a454bc9 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -558,9 +558,6 @@ parameters: Process_first_active: description: one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep dims: [period, process] - Transformer_first_active: - description: one in the first period a transformer stands in, zero elsewhere — the spec extends the carrier growth limit to transformers, which PyPSA does not, so PyPSA has no counterpart, data prep - dims: [period, transformer] Carrier_max_growth: description: most capacity of a carrier that may be added in a period; no value means no limit dims: [carrier] @@ -1073,10 +1070,6 @@ relations: description: the bus a store sits on key: store values: bus - Transformer_carrier: - description: the carrier a transformer carries - key: transformer - values: carrier Transformer_bus0: description: the bus a transformer's flow is measured at key: transformer @@ -1903,9 +1896,9 @@ expressions: Carrier_additions: description: >- what a carrier adds in a period — every extendable component of that - carrier, counting each build in the first period it stands in. PyPSA sums - the components that carry a carrier attribute; the transformer term is the - spec's own extension, since PyPSA gives a transformer no carrier + carrier, counting each build in the first period it stands in. Like PyPSA, + it sums only the components that carry a carrier attribute, so a + transformer, which has none, counts in no carrier expression: >- sum(Generator_p_nom_ext * Generator_first_active, by=Generator_carrier, over=generator, into=carrier) + sum(Link_p_nom_ext * Link_first_active, by=Link_carrier, over=link, into=carrier) @@ -1913,7 +1906,6 @@ expressions: + sum(Store_e_nom_ext * Store_first_active, by=Store_carrier, over=store, into=carrier) + sum(Line_s_nom_ext * Line_first_active, by=Line_carrier, over=line, into=carrier) + sum(Process_p_nom_ext * Process_first_active, by=Process_carrier, over=process, into=carrier) - + sum(Transformer_s_nom_ext * Transformer_first_active, by=Transformer_carrier, over=transformer, into=carrier) Line_s_monitored: description: >- the flow a line's post-contingency rows read — its flow where it stands, diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 299b8d13..f8475175 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -305,7 +305,6 @@ names: Store_first_active: '\mathrm{new}^{e}' Line_first_active: '\mathrm{new}^{s}' Process_first_active: '\mathrm{new}^{z}' - Transformer_first_active: '\mathrm{new}^{\sigma}' Carrier_max_growth: '\overline{\Delta}' Carrier_max_relative_growth: '\mathrm{r}' Generator_maintainable: '\mathrm{mnt}' From 76c5c249071b9b330bcd8733a944468c35d6bf4f Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 13:56:29 +0200 Subject: [PATCH 22/46] docs(pypsa): a global constraint may count one investment period, weighted by its years --- docs/examples/pypsa.md | 367 ++++++++++++++++-- examples/pypsa.yaml | 192 +++++++-- examples/references/pypsa/references.json | 45 +++ .../rung_35_period_global_constraints.py | 74 ++++ examples/symbols/pypsa.yaml | 7 +- tests/fixtures/pypsa_standard_shape.yaml | 2 - 6 files changed, 627 insertions(+), 60 deletions(-) create mode 100644 examples/references/pypsa/rung_35_period_global_constraints.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index db1ce623..31831d22 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -414,8 +414,8 @@ each type is three blocks by sense. | PyPSA type | status | note | | ------------------------------------- | ----------- | ------------------------------------------------- | -| [`primary_energy`](#primary_energy) | split | a block per sense — sense as data is beyond #70; carrier weights and the horizon-end charge read are prep | -| [`operational_limit`](#operational_limit) | split | a block per sense | +| [`primary_energy`](#primary_energy) | split | a block per sense — sense as data is beyond #70; carrier weights are prep; one period in rung 35 | +| [`operational_limit`](#operational_limit) | split | a block per sense; one period in rung 35 | | [`transmission_volume_expansion_limit`](#transmission_volume_expansion_limit) | split | a block per sense; membership from PyPSA's carrier string is prep | | [`transmission_expansion_cost_limit`](#transmission_expansion_cost_limit) | split | a block per sense | | [`tech_capacity_expansion_limit`](#tech_capacity_expansion_limit) | split | a block per sense | @@ -1360,6 +1360,7 @@ own dimensions. | [objective](#objective) | done | period weight on operation; capacity once per period it stands in | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) for storage built in a later period or retired early | done | rung 32 | +| [`primary_energy`](#primary_energy), [`operational_limit`](#operational_limit) for one investment period, weighted by period years | done | rung 35 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12747.19109626398`, 80 rows. @@ -2911,6 +2912,120 @@ def build(): +### Rung 35 — a global constraint for one investment period + +`n.optimize(multi_investment_periods=True)` with `primary_energy` and +`operational_limit` rows that name an `investment_period`. PyPSA sums such a +row over the snapshots of that period only, and over the whole horizon where +the row names none (`global_constraints.py:373-378`, `600-606`). Each snapshot +counts with its generator weighting times the `years` weighting of its period +(`:326`, `:615`). A storage unit or store that reopens per period closes at the +last snapshot of each counted period, weighted by that period's years +(`:474-477`, `:673-676`). One that carries its level across periods closes +once, at the last counted snapshot (`:459-470`, `:658-668`). The file states +the counted snapshots as `GlobalConstraint_counts_snapshot`, data prep, and +the years as `period_weight_years`. A plain run feeds all true and one, so the +rows collapse to the standard ones. + +The rung caps CO2 in 2030 alone, caps it again over the horizon, and limits a +hydro carrier with a per-period storage unit and store in 2020. The years +weightings are 5 and 10. With the same network and no `investment_period`, +PyPSA solves to `10675.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`primary_energy`](#primary_energy), [`operational_limit`](#operational_limit) over one investment period | done | `GlobalConstraint_energy_weight` is zero outside the counted snapshots, and the years weigh each snapshot | +| the closing level of storage in those rows | done | `StorageUnit_closing_weight`, `Store_closing_weight`: each counted period's last snapshot where the storage reopens per period, the last counted snapshot otherwise | +| a `primary_energy` row for one period over storage that reopens per period | refused, as PyPSA | assumed: [`StorageUnit_primary_energy_per_period_closes_over_the_horizon`](#storageunit_primary_energy_per_period_closes_over_the_horizon), and the `Store` one | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `4886.764705882353`, 155 rows. + +
+The network, as PyPSA code + +`rung_35_period_global_constraints.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 35: a global constraint for one investment period — a CO2 cap on 2030 alone, one over the horizon, and a 2020 limit on a carrier with storage that reopens per period.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: eight snapshots over two periods of unequal years, two emitting units, a hydro carrier with a unit and storage.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [5.0, 10.0] + n.snapshot_weightings['generators'] = [1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0] + n.add('Carrier', 'coal35', co2_emissions=1.0) + n.add('Carrier', 'gas35', co2_emissions=0.4) + n.add('Carrier', 'hydro35') + n.add('Bus', 'hub') + n.add('Generator', 'coal35', bus='hub', carrier='coal35', p_nom=100, marginal_cost=10, efficiency=0.4) + n.add('Generator', 'gas35', bus='hub', carrier='gas35', p_nom=100, marginal_cost=30, efficiency=0.5) + n.add('Generator', 'clean35', bus='hub', p_nom=200, marginal_cost=60) + n.add('Generator', 'river35', bus='hub', carrier='hydro35', p_nom=30, marginal_cost=5) + n.add( + 'StorageUnit', + 'dam35', + bus='hub', + carrier='hydro35', + p_nom=15, + max_hours=4, + state_of_charge_initial=20, + state_of_charge_initial_per_period=True, + ) + n.add('Store', 'pond35', bus='hub', carrier='hydro35', e_nom=30, e_initial=10, e_initial_per_period=True) + n.add('Load', 'town35', bus='hub', p_set=[60, 80, 70, 50, 90, 110, 100, 80]) + n.add( + 'GlobalConstraint', + 'co2_2030', + type='primary_energy', + carrier_attribute='co2_emissions', + sense='<=', + constant=3500, + investment_period=2030, + ) + n.add( + 'GlobalConstraint', + 'co2_all', + type='primary_energy', + carrier_attribute='co2_emissions', + sense='<=', + constant=6000, + ) + n.add( + 'GlobalConstraint', + 'hydro_2020', + type='operational_limit', + carrier_attribute='hydro35', + sense='<=', + constant=600, + investment_period=2020, + ) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -2924,7 +3039,9 @@ where it should live — language, data prep, or harness — is one open questio | `ValueError`, `constraints.py:1850` | fixed modular `p_nom` not a multiple of `p_nom_mod` | a fractional module cap | X1 | | `ValueError`, `constraints.py:1557` | load on a bus with nothing attached | row not built, unserved | X2 | | `ValueError`, `optimize.py:436` | no component carries a cost | feasibility problem | X3 | -| `NotImplementedError`, `global_constraints.py:457` | depletion with period weightings `!= 1` | out | | +| `NotImplementedError`, `global_constraints.py:457`, `:509`, `:656`, `:704` | storage that carries its level across periods in a `primary_energy` or `operational_limit` row, with period `years` `!= 1` | assumed: [`StorageUnit_primary_energy_carried_over_has_unit_years`](#storageunit_primary_energy_carried_over_has_unit_years), [`StorageUnit_operational_limit_carried_over_has_unit_years`](#storageunit_operational_limit_carried_over_has_unit_years), and the `Store` ones | | +| `KeyError`, `global_constraints.py:474`, `:526` | a `primary_energy` row for one period over storage that reopens per period | assumed: [`StorageUnit_primary_energy_per_period_closes_over_the_horizon`](#storageunit_primary_energy_per_period_closes_over_the_horizon), and the `Store` one | | +| `UnboundLocalError`, `global_constraints.py:375`, `:602` | a `primary_energy` or `operational_limit` row that names an `investment_period` without `multi_investment_periods` | data prep, at `GlobalConstraint_counts_snapshot` | | | `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` and `Transformer_loss_max` | X4 | | `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | | `ValueError`, `abstract.py:427`, `:445` | a security-constrained run over scenarios | rows per scenario, not refused | | @@ -3068,6 +3185,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | | $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor | | $`\mathrm{w}^{y}`$ | `period_weight_objective` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.objective` — what a period's cost weighs | +| $`\mathrm{w}^{\mathrm{yr}}`$ | `period_weight_years` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.years` — what a period's energy weighs in a `primary_energy` or `operational_limit` row; PyPSA reads it only under `multi_investment_periods`, so data prep feeds one otherwise | | $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep | | $`\mathrm{on}^{f}`$ | `Link_active` over $`\mathcal{T} \times \mathcal{L}`$ — whether a link stands in a snapshot's period — PyPSA's `active`, data prep | | $`\mathrm{on}^{h}`$ | `StorageUnit_active` over $`\mathcal{T} \times \mathcal{S}`$ — whether a storage unit stands in a snapshot's period — PyPSA's `active`, data prep | @@ -3180,24 +3298,24 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{b}^{\sigma}`$ | `Transformer_loss_offset` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | | $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{I}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | | $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\mathcal{I}`$ — which way the row binds — `<=`, `>=` or `==` | -| $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, a non-extendable build — is folded in here by data prep | -| $`\mathrm{last}`$ | `snapshot_is_last` over $`\mathcal{T}`$ — one at the horizon's last snapshot, zero elsewhere — data prep, how an expression reads a final level | +| $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, times its period's years for each counted period where the storage reopens per period, or a non-extendable build — is folded in here by data prep | +| $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\mathcal{I} \times \mathcal{T}`$ — whether a row counts a snapshot — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:377`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:375`) | | $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\mathcal{I} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency, data prep; a generator of an unweighted carrier has no row | | $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\mathcal{I} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | | $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\mathcal{I} \times \mathcal{V}`$ — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | | $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row | | $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's set — data prep; one outside it has no row | | $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's set — data prep; one outside it has no row | -| $`\mathrm{len}`$ | `Line_volume_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's length where its carrier is in the row's set — data prep; a line outside it has no row | -| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's length where its carrier is in the row's set — data prep; a link outside it has no row | -| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set — data prep; a line outside it has no row | -| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set — data prep; a link outside it has no row | -| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{L}`$ — one where the link is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's carrier-and-bus set — data prep; one outside it has no row | -| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{J}`$ — one where the process is in the row's carrier-and-bus set — data prep; one outside it has no row | +| $`\mathrm{len}`$ | `Line_volume_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's length where its carrier is in the row's set — data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's length where its carrier is in the row's set — data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row | +| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row | +| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{L}`$ — one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{J}`$ — one where the process is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables @@ -3290,8 +3408,12 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial energy, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A store built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon | | $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | | $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed transfer wraps from the horizon's end, and where it is not the energy still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | -| $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{I}`$ — what a `primary_energy` row totals — weighted generator energy, less the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant | -| $`\mathit{operational\_limit}`$ | `operational_limit` over $`\Xi \times \mathcal{I}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | +| $`\mathit{w}^{\mathrm{gc}}`$ | `GlobalConstraint_energy_weight` over $`\mathcal{I} \times \mathcal{T}`$ — what one unit of power at a snapshot counts for in a row — the generator weighting times the years of the snapshot's period, where the row counts the snapshot, and nothing where it does not | +| $`\mathit{last}`$ | `GlobalConstraint_snapshot_closes` over $`\mathcal{I} \times \mathcal{T}`$ — one at the last snapshot a row counts, and zero elsewhere | +| $`\mathit{w}^{h}`$ | `StorageUnit_closing_weight` over $`\mathcal{I} \times \mathcal{T} \times \mathcal{S}`$ — what the charge a unit holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the unit reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere | +| $`\mathit{w}^{e}`$ | `Store_closing_weight` over $`\mathcal{I} \times \mathcal{T} \times \mathcal{V}`$ — what the energy a store holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the store reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere | +| $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{I}`$ — what a `primary_energy` row totals — weighted generator energy over the snapshots it counts, less the charge left in weighted storage at the close; the initial charge it is compared against is folded into the row's constant | +| $`\mathit{operational\_limit}`$ | `operational_limit` over $`\Xi \times \mathcal{I}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver over the snapshots it counts, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | | $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\mathcal{I}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | | $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\mathcal{I}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | @@ -3313,6 +3435,8 @@ $`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own ord $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ counts within the group a relation puts $`t`$ in: the subscript names the map, $`\mathcal{T}_{\mathrm{relation}(t)}`$ is the group it lands in, and that group has a first position of its own. +$`\lvert \mathcal{T} \rvert`$ denotes the size of the set being counted along, and a position counted from the end prints against it — $`\lvert \mathcal{T} \rvert - 1`$ is the last position, one less than the size because the first is $`0`$. + ### Objective ```yaml @@ -7978,22 +8102,108 @@ Process_output_arrival: \overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} ``` +### `GlobalConstraint_energy_weight` + +```yaml +GlobalConstraint_energy_weight: + description: >- + what one unit of power at a snapshot counts for in a row — the + generator weighting times the years of the snapshot's period, where the + row counts the snapshot, and nothing where it does not + dims: [global_constraint, snapshot] + cases: + counted: + when: GlobalConstraint_counts_snapshot + expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot_period, over=period, into=snapshot) + otherwise: 0 +``` + +```math +\mathit{w}^{\mathrm{gc}}_{i,t} = \begin{cases} \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{in}_{i,t} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T} +``` + +### `GlobalConstraint_snapshot_closes` + +```yaml +GlobalConstraint_snapshot_closes: + description: one at the last snapshot a row counts, and zero elsewhere + dims: [global_constraint, snapshot] + cases: + last_counted: + when: GlobalConstraint_counts_snapshot AND NOT shift(GlobalConstraint_counts_snapshot, along=snapshot, offset=-1) + expression: 1 + otherwise: 0 +``` + +```math +\mathit{last}_{i,t} = \begin{cases} 1 & \text{if } \mathrm{in}_{i,t} \wedge \neg \mathrm{in}_{i,t + 1} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T} +``` + +### `StorageUnit_closing_weight` + +```yaml +StorageUnit_closing_weight: + description: >- + what the charge a unit holds at a snapshot counts for in a row as its + closing level — the years of the period at the last snapshot of each + counted period where the unit reopens per period, one at the last + counted snapshot where it does not, and nothing elsewhere + dims: [global_constraint, snapshot, storage_unit] + cases: + per_period: + when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 + expression: at(period_weight_years, by=snapshot_period, over=period, into=snapshot) + carried_over: + when: NOT StorageUnit_state_of_charge_initial_per_period + expression: GlobalConstraint_snapshot_closes + otherwise: 0 +``` + +```math +\mathit{w}^{h}_{i,t,s} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}_{s} \wedge \mathrm{in}_{i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}_{i,t} & \text{if } \neg \mathrm{reset}_{s} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T},\ s \in \mathcal{S} +``` + +### `Store_closing_weight` + +```yaml +Store_closing_weight: + description: >- + what the energy a store holds at a snapshot counts for in a row as its + closing level — the years of the period at the last snapshot of each + counted period where the store reopens per period, one at the last + counted snapshot where it does not, and nothing elsewhere + dims: [global_constraint, snapshot, store] + cases: + per_period: + when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 + expression: at(period_weight_years, by=snapshot_period, over=period, into=snapshot) + carried_over: + when: NOT Store_e_initial_per_period + expression: GlobalConstraint_snapshot_closes + otherwise: 0 +``` + +```math +\mathit{w}^{e}_{i,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}^{e}_{v} \wedge \mathrm{in}_{i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}_{i,t} & \text{if } \neg \mathrm{reset}^{e}_{v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T},\ v \in \mathcal{V} +``` + ### `primary_energy` ```yaml primary_energy: description: >- - what a `primary_energy` row totals — weighted generator energy, less - the charge left in weighted storage at the horizon's end; the initial - charge it is compared against is folded into the row's constant + what a `primary_energy` row totals — weighted generator energy over the + snapshots it counts, less the charge left in weighted storage at the + close; the initial charge it is compared against is folded into the + row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_primary_energy_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_primary_energy_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_primary_energy_weight, over=snapshot), over=store) + sum(sum(Generator_p * GlobalConstraint_energy_weight * Generator_primary_energy_weight, over=snapshot), over=generator) + - sum(sum(StorageUnit_state_of_charge * StorageUnit_closing_weight * StorageUnit_primary_energy_weight, over=snapshot), over=storage_unit) + - sum(sum(Store_e * Store_closing_weight * Store_primary_energy_weight, over=snapshot), over=store) ``` ```math -\mathit{primary\_energy}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{a}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{a}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} +\mathit{primary\_energy}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathit{w}^{\mathrm{gc}}_{i,t} \cdot \mathrm{a}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathit{w}^{h}_{i,t,s} \cdot \mathrm{a}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{i,t,v} \cdot \mathrm{a}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `operational_limit` @@ -8002,16 +8212,17 @@ primary_energy: operational_limit: description: >- what an `operational_limit` row totals — the weighted energy its - generators deliver, plus what its non-cyclic storage draws down; the - initial charge it draws from is folded into the row's constant + generators deliver over the snapshots it counts, plus what its + non-cyclic storage draws down; the initial charge it draws from is + folded into the row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_operational_limit_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_operational_limit_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_operational_limit_weight, over=snapshot), over=store) + sum(sum(Generator_p * GlobalConstraint_energy_weight * Generator_operational_limit_weight, over=snapshot), over=generator) + - sum(sum(StorageUnit_state_of_charge * StorageUnit_closing_weight * StorageUnit_operational_limit_weight, over=snapshot), over=storage_unit) + - sum(sum(Store_e * Store_closing_weight * Store_operational_limit_weight, over=snapshot), over=store) ``` ```math -\mathit{operational\_limit}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{b}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathrm{last}_{t} \cdot \mathrm{b}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} +\mathit{operational\_limit}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathit{w}^{\mathrm{gc}}_{i,t} \cdot \mathrm{b}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathit{w}^{h}_{i,t,s} \cdot \mathrm{b}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{i,t,v} \cdot \mathrm{b}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `transmission_volume_expansion` @@ -8831,6 +9042,102 @@ Store_opens_late_where_it_opens: ```math \mathrm{open}^{e}_{t,v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} \wedge \neg \mathrm{on}^{e}_{t - 1,v} \wedge \mathrm{pos}(t) > 0 ``` + +### `StorageUnit_primary_energy_per_period_closes_over_the_horizon` + +```yaml +StorageUnit_primary_energy_per_period_closes_over_the_horizon: + holds: "count(NOT GlobalConstraint_counts_snapshot, over=snapshot) == 0" + where: "StorageUnit_primary_energy_weight AND StorageUnit_state_of_charge_initial_per_period" + description: >- + PyPSA reads the closing charge of a unit that reopens per period at + the last snapshot of every period, and fails on a `primary_energy` row + that names an `investment_period` (`global_constraints.py:474`) +``` + +```math +\lvert \{ t \in \mathcal{T} \,:\, \neg \mathrm{in}_{i,t} \} \rvert = 0 \qquad \forall\, s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{a}^{h}_{i,s} \text{ is defined} \wedge \mathrm{reset}_{s} +``` + +### `StorageUnit_primary_energy_carried_over_has_unit_years` + +```yaml +StorageUnit_primary_energy_carried_over_has_unit_years: + holds: "period_weight_years == 1" + where: "StorageUnit_primary_energy_weight AND NOT StorageUnit_state_of_charge_initial_per_period" + description: >- + a unit that carries its charge from one period to the next closes + once, at the last counted snapshot, and no period's years weighs that + level — PyPSA refuses it where any period's years is not one + (`global_constraints.py:448`) +``` + +```math +\mathrm{w}^{\mathrm{yr}}_{y} = 1 \qquad \forall\, s \in \mathcal{S},\ i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \mathrm{a}^{h}_{i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{s} +``` + +### `StorageUnit_operational_limit_carried_over_has_unit_years` + +```yaml +StorageUnit_operational_limit_carried_over_has_unit_years: + holds: "at(period_weight_years == 1, by=snapshot_period, over=period, into=snapshot)" + where: "StorageUnit_operational_limit_weight AND NOT StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot" + description: >- + the same for an `operational_limit` row, over the periods it counts — + PyPSA refuses it (`global_constraints.py:647`) +``` + +```math +\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{b}^{h}_{i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{s} \wedge \mathrm{in}_{i,t} +``` + +### `Store_primary_energy_per_period_closes_over_the_horizon` + +```yaml +Store_primary_energy_per_period_closes_over_the_horizon: + holds: "count(NOT GlobalConstraint_counts_snapshot, over=snapshot) == 0" + where: "Store_primary_energy_weight AND Store_e_initial_per_period" + description: >- + PyPSA reads the closing energy of a store that reopens per period at + the last snapshot of every period, and fails on a `primary_energy` row + that names an `investment_period` (`global_constraints.py:526`) +``` + +```math +\lvert \{ t \in \mathcal{T} \,:\, \neg \mathrm{in}_{i,t} \} \rvert = 0 \qquad \forall\, v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{a}^{e}_{i,v} \text{ is defined} \wedge \mathrm{reset}^{e}_{v} +``` + +### `Store_primary_energy_carried_over_has_unit_years` + +```yaml +Store_primary_energy_carried_over_has_unit_years: + holds: "period_weight_years == 1" + where: "Store_primary_energy_weight AND NOT Store_e_initial_per_period" + description: >- + a store that carries its energy from one period to the next closes + once, at the last counted snapshot, and no period's years weighs that + level — PyPSA refuses it where any period's years is not one + (`global_constraints.py:500`) +``` + +```math +\mathrm{w}^{\mathrm{yr}}_{y} = 1 \qquad \forall\, v \in \mathcal{V},\ i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \mathrm{a}^{e}_{i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{v} +``` + +### `Store_operational_limit_carried_over_has_unit_years` + +```yaml +Store_operational_limit_carried_over_has_unit_years: + holds: "at(period_weight_years == 1, by=snapshot_period, over=period, into=snapshot)" + where: "Store_operational_limit_weight AND NOT Store_e_initial_per_period AND GlobalConstraint_counts_snapshot" + description: >- + the same for an `operational_limit` row, over the periods it counts — + PyPSA refuses it (`global_constraints.py:695`) +``` + +```math +\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{b}^{e}_{i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \mathrm{in}_{i,t} +``` Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 5a454bc9..0b26fd25 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -491,6 +491,12 @@ parameters: period_weight_objective: description: PyPSA's `investment_period_weightings.objective` — what a period's cost weighs dims: [period] + period_weight_years: + description: >- + PyPSA's `investment_period_weightings.years` — what a period's energy + weighs in a `primary_energy` or `operational_limit` row; PyPSA reads it + only under `multi_investment_periods`, so data prep feeds one otherwise + dims: [period] Generator_active: description: whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep dims: [snapshot, generator] @@ -930,13 +936,20 @@ parameters: GlobalConstraint_constant: description: >- the constant the total is held against; what a variable cannot carry — - an initial charge, a non-extendable build — is folded in here by data - prep + an initial charge, times its period's years for each counted period + where the storage reopens per period, or a non-extendable build — is + folded in here by data prep dims: [global_constraint] - snapshot_is_last: - description: one at the horizon's last snapshot, zero elsewhere — data prep, how an expression reads a final level - dims: [snapshot] - dtype: int + GlobalConstraint_counts_snapshot: + description: >- + whether a row counts a snapshot — PyPSA's `investment_period`: every + snapshot where the row names none, and only that period's where it + names one, data prep. A row that names a period the run does not model + has no label here, as PyPSA skips it (`global_constraints.py:377`); + PyPSA reads the column only under `multi_investment_periods`, and fails + on a row that names a period without it (`global_constraints.py:375`) + dims: [global_constraint, snapshot] + dtype: bool Generator_primary_energy_weight: description: >- the constrained attribute per unit of energy at the bus — the carrier's @@ -959,36 +972,68 @@ parameters: description: one where the store is in the row's set — data prep; one outside it has no row dims: [global_constraint, store] Line_volume_weight: - description: the line's length where its carrier is in the row's set — data prep; a line outside it has no row + description: >- + the line's length where its carrier is in the row's set — data prep; a + line outside it, or one that does not stand in the row's + `investment_period`, has no row dims: [global_constraint, line] Link_volume_weight: - description: the link's length where its carrier is in the row's set — data prep; a link outside it has no row + description: >- + the link's length where its carrier is in the row's set — data prep; a + link outside it, or one that does not stand in the row's + `investment_period`, has no row dims: [global_constraint, link] Line_expansion_cost_weight: - description: the line's capital cost where its carrier is in the row's set — data prep; a line outside it has no row + description: >- + the line's capital cost where its carrier is in the row's set, times + the objective weights of the periods it stands in where the row names + no `investment_period` under `multi_investment_periods` — data prep; a + line outside the set, or one that does not stand in the row's period, + has no row dims: [global_constraint, line] Link_expansion_cost_weight: - description: the link's capital cost where its carrier is in the row's set — data prep; a link outside it has no row + description: >- + the link's capital cost where its carrier is in the row's set, times + the objective weights of the periods it stands in where the row names + no `investment_period` under `multi_investment_periods` — data prep; a + link outside the set, or one that does not stand in the row's period, + has no row dims: [global_constraint, link] Generator_tech_capacity_weight: description: >- one where the generator is in the row's carrier-and-bus set — data - prep; one outside it has no row + prep; one outside it, or one that does not stand in the row's + `investment_period`, has no row dims: [global_constraint, generator] Link_tech_capacity_weight: - description: one where the link is in the row's carrier-and-bus set — data prep; one outside it has no row + description: >- + one where the link is in the row's carrier-and-bus set — data prep; one + outside it, or one that does not stand in the row's `investment_period`, + has no row dims: [global_constraint, link] Line_tech_capacity_weight: - description: one where the line is in the row's carrier-and-bus set — data prep; one outside it has no row + description: >- + one where the line is in the row's carrier-and-bus set — data prep; one + outside it, or one that does not stand in the row's `investment_period`, + has no row dims: [global_constraint, line] StorageUnit_tech_capacity_weight: - description: one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it has no row + description: >- + one where the storage unit is in the row's carrier-and-bus set — data prep; one + outside it, or one that does not stand in the row's `investment_period`, + has no row dims: [global_constraint, storage_unit] Store_tech_capacity_weight: - description: one where the store is in the row's carrier-and-bus set — data prep; one outside it has no row + description: >- + one where the store is in the row's carrier-and-bus set — data prep; one + outside it, or one that does not stand in the row's `investment_period`, + has no row dims: [global_constraint, store] Process_tech_capacity_weight: - description: one where the process is in the row's carrier-and-bus set — data prep; one outside it has no row + description: >- + one where the process is in the row's carrier-and-bus set — data prep; one + outside it, or one that does not stand in the row's `investment_period`, + has no row dims: [global_constraint, process] relations: @@ -1834,24 +1879,75 @@ expressions: when: Process_output_cyclic_delay expression: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge='wrap') otherwise: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge=0) + GlobalConstraint_energy_weight: + description: >- + what one unit of power at a snapshot counts for in a row — the + generator weighting times the years of the snapshot's period, where the + row counts the snapshot, and nothing where it does not + dims: [global_constraint, snapshot] + cases: + counted: + when: GlobalConstraint_counts_snapshot + expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot_period, over=period, into=snapshot) + otherwise: 0 + GlobalConstraint_snapshot_closes: + description: one at the last snapshot a row counts, and zero elsewhere + dims: [global_constraint, snapshot] + cases: + last_counted: + when: GlobalConstraint_counts_snapshot AND NOT shift(GlobalConstraint_counts_snapshot, along=snapshot, offset=-1) + expression: 1 + otherwise: 0 + StorageUnit_closing_weight: + description: >- + what the charge a unit holds at a snapshot counts for in a row as its + closing level — the years of the period at the last snapshot of each + counted period where the unit reopens per period, one at the last + counted snapshot where it does not, and nothing elsewhere + dims: [global_constraint, snapshot, storage_unit] + cases: + per_period: + when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 + expression: at(period_weight_years, by=snapshot_period, over=period, into=snapshot) + carried_over: + when: NOT StorageUnit_state_of_charge_initial_per_period + expression: GlobalConstraint_snapshot_closes + otherwise: 0 + Store_closing_weight: + description: >- + what the energy a store holds at a snapshot counts for in a row as its + closing level — the years of the period at the last snapshot of each + counted period where the store reopens per period, one at the last + counted snapshot where it does not, and nothing elsewhere + dims: [global_constraint, snapshot, store] + cases: + per_period: + when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 + expression: at(period_weight_years, by=snapshot_period, over=period, into=snapshot) + carried_over: + when: NOT Store_e_initial_per_period + expression: GlobalConstraint_snapshot_closes + otherwise: 0 primary_energy: description: >- - what a `primary_energy` row totals — weighted generator energy, less - the charge left in weighted storage at the horizon's end; the initial - charge it is compared against is folded into the row's constant + what a `primary_energy` row totals — weighted generator energy over the + snapshots it counts, less the charge left in weighted storage at the + close; the initial charge it is compared against is folded into the + row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_primary_energy_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_primary_energy_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_primary_energy_weight, over=snapshot), over=store) + sum(sum(Generator_p * GlobalConstraint_energy_weight * Generator_primary_energy_weight, over=snapshot), over=generator) + - sum(sum(StorageUnit_state_of_charge * StorageUnit_closing_weight * StorageUnit_primary_energy_weight, over=snapshot), over=storage_unit) + - sum(sum(Store_e * Store_closing_weight * Store_primary_energy_weight, over=snapshot), over=store) operational_limit: description: >- what an `operational_limit` row totals — the weighted energy its - generators deliver, plus what its non-cyclic storage draws down; the - initial charge it draws from is folded into the row's constant + generators deliver over the snapshots it counts, plus what its + non-cyclic storage draws down; the initial charge it draws from is + folded into the row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_operational_limit_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_operational_limit_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_operational_limit_weight, over=snapshot), over=store) + sum(sum(Generator_p * GlobalConstraint_energy_weight * Generator_operational_limit_weight, over=snapshot), over=generator) + - sum(sum(StorageUnit_state_of_charge * StorageUnit_closing_weight * StorageUnit_operational_limit_weight, over=snapshot), over=storage_unit) + - sum(sum(Store_e * Store_closing_weight * Store_operational_limit_weight, over=snapshot), over=store) transmission_volume_expansion: description: what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches expression: >- @@ -3614,6 +3710,48 @@ assumptions: a store that opens past the first snapshot of the horizon opens on its initial level, or its last level where it is cyclic, only where `Store_opens_late` marks the snapshot + StorageUnit_primary_energy_per_period_closes_over_the_horizon: + holds: "count(NOT GlobalConstraint_counts_snapshot, over=snapshot) == 0" + where: "StorageUnit_primary_energy_weight AND StorageUnit_state_of_charge_initial_per_period" + description: >- + PyPSA reads the closing charge of a unit that reopens per period at + the last snapshot of every period, and fails on a `primary_energy` row + that names an `investment_period` (`global_constraints.py:474`) + StorageUnit_primary_energy_carried_over_has_unit_years: + holds: "period_weight_years == 1" + where: "StorageUnit_primary_energy_weight AND NOT StorageUnit_state_of_charge_initial_per_period" + description: >- + a unit that carries its charge from one period to the next closes + once, at the last counted snapshot, and no period's years weighs that + level — PyPSA refuses it where any period's years is not one + (`global_constraints.py:448`) + StorageUnit_operational_limit_carried_over_has_unit_years: + holds: "at(period_weight_years == 1, by=snapshot_period, over=period, into=snapshot)" + where: "StorageUnit_operational_limit_weight AND NOT StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot" + description: >- + the same for an `operational_limit` row, over the periods it counts — + PyPSA refuses it (`global_constraints.py:647`) + Store_primary_energy_per_period_closes_over_the_horizon: + holds: "count(NOT GlobalConstraint_counts_snapshot, over=snapshot) == 0" + where: "Store_primary_energy_weight AND Store_e_initial_per_period" + description: >- + PyPSA reads the closing energy of a store that reopens per period at + the last snapshot of every period, and fails on a `primary_energy` row + that names an `investment_period` (`global_constraints.py:526`) + Store_primary_energy_carried_over_has_unit_years: + holds: "period_weight_years == 1" + where: "Store_primary_energy_weight AND NOT Store_e_initial_per_period" + description: >- + a store that carries its energy from one period to the next closes + once, at the last counted snapshot, and no period's years weighs that + level — PyPSA refuses it where any period's years is not one + (`global_constraints.py:500`) + Store_operational_limit_carried_over_has_unit_years: + holds: "at(period_weight_years == 1, by=snapshot_period, over=period, into=snapshot)" + where: "Store_operational_limit_weight AND NOT Store_e_initial_per_period AND GlobalConstraint_counts_snapshot" + description: >- + the same for an `operational_limit` row, over the periods it counts — + PyPSA refuses it (`global_constraints.py:695`) objective: sense: minimize diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 6c90cd23..c968e575 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1393,5 +1393,50 @@ "Process-status-p-fixed-upper": 8, "Process-status-p_nom-variable-upper": 4 } + }, + "rung_35_period_global_constraints": { + "columns": { + "Generator-p": 32, + "StorageUnit-p_dispatch": 8, + "StorageUnit-p_store": 8, + "StorageUnit-state_of_charge": 8, + "Store-e": 8, + "Store-p": 8 + }, + "global_constraints": { + "co2_2030": { + "sense": "<=", + "type": "primary_energy" + }, + "co2_all": { + "sense": "<=", + "type": "primary_energy" + }, + "hydro_2020": { + "sense": "<=", + "type": "operational_limit" + } + }, + "objective": 4886.764705882353, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-fix-p-lower": 32, + "Generator-fix-p-upper": 32, + "GlobalConstraint-co2_2030": 1, + "GlobalConstraint-co2_all": 1, + "GlobalConstraint-hydro_2020": 1, + "StorageUnit-energy_balance": 8, + "StorageUnit-fix-p_dispatch-lower": 8, + "StorageUnit-fix-p_dispatch-upper": 8, + "StorageUnit-fix-p_store-lower": 8, + "StorageUnit-fix-p_store-upper": 8, + "StorageUnit-fix-state_of_charge-lower": 8, + "StorageUnit-fix-state_of_charge-upper": 8, + "Store-energy_balance": 8, + "Store-fix-e-lower": 8, + "Store-fix-e-upper": 8 + } } } diff --git a/examples/references/pypsa/rung_35_period_global_constraints.py b/examples/references/pypsa/rung_35_period_global_constraints.py new file mode 100644 index 00000000..9e90c82a --- /dev/null +++ b/examples/references/pypsa/rung_35_period_global_constraints.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 35: a global constraint for one investment period — a CO2 cap on 2030 alone, one over the horizon, and a 2020 limit on a carrier with storage that reopens per period.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: eight snapshots over two periods of unequal years, two emitting units, a hydro carrier with a unit and storage.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [5.0, 10.0] + n.snapshot_weightings['generators'] = [1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0] + n.add('Carrier', 'coal35', co2_emissions=1.0) + n.add('Carrier', 'gas35', co2_emissions=0.4) + n.add('Carrier', 'hydro35') + n.add('Bus', 'hub') + n.add('Generator', 'coal35', bus='hub', carrier='coal35', p_nom=100, marginal_cost=10, efficiency=0.4) + n.add('Generator', 'gas35', bus='hub', carrier='gas35', p_nom=100, marginal_cost=30, efficiency=0.5) + n.add('Generator', 'clean35', bus='hub', p_nom=200, marginal_cost=60) + n.add('Generator', 'river35', bus='hub', carrier='hydro35', p_nom=30, marginal_cost=5) + n.add( + 'StorageUnit', + 'dam35', + bus='hub', + carrier='hydro35', + p_nom=15, + max_hours=4, + state_of_charge_initial=20, + state_of_charge_initial_per_period=True, + ) + n.add('Store', 'pond35', bus='hub', carrier='hydro35', e_nom=30, e_initial=10, e_initial_per_period=True) + n.add('Load', 'town35', bus='hub', p_set=[60, 80, 70, 50, 90, 110, 100, 80]) + n.add( + 'GlobalConstraint', + 'co2_2030', + type='primary_energy', + carrier_attribute='co2_emissions', + sense='<=', + constant=3500, + investment_period=2030, + ) + n.add( + 'GlobalConstraint', + 'co2_all', + type='primary_energy', + carrier_attribute='co2_emissions', + sense='<=', + constant=6000, + ) + n.add( + 'GlobalConstraint', + 'hydro_2020', + type='operational_limit', + carrier_attribute='hydro35', + sense='<=', + constant=600, + investment_period=2020, + ) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index f8475175..8b1299a9 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -219,7 +219,11 @@ names: GlobalConstraint_type: '\mathrm{type}' GlobalConstraint_sense: '\mathrm{sense}' GlobalConstraint_constant: '\mathrm{K}' - snapshot_is_last: '\mathrm{last}' + GlobalConstraint_counts_snapshot: '\mathrm{in}' + GlobalConstraint_energy_weight: '\mathit{w}^{\mathrm{gc}}' + StorageUnit_closing_weight: '\mathit{w}^{h}' + Store_closing_weight: '\mathit{w}^{e}' + GlobalConstraint_snapshot_closes: '\mathit{last}' Generator_primary_energy_weight: '\mathrm{a}' StorageUnit_primary_energy_weight: '\mathrm{a}^{h}' Store_primary_energy_weight: '\mathrm{a}^{e}' @@ -285,6 +289,7 @@ names: CVaR_theta: '\theta' CVaR: "CVaR" period_weight_objective: '\mathrm{w}^{y}' + period_weight_years: '\mathrm{w}^{\mathrm{yr}}' Generator_active: '\mathrm{on}' Link_active: '\mathrm{on}^{f}' StorageUnit_active: '\mathrm{on}^{h}' diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index b34c500e..fd3c36c8 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -924,8 +924,6 @@ parameters: Transformer_s_set: - snapshot - transformer - snapshot_is_last: - - snapshot snapshot_weightings_generators: - snapshot snapshot_weightings_objective: From 510b56702741589499217e45bee5eace906fb09e Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 14:03:31 +0200 Subject: [PATCH 23/46] docs(pypsa): a process, a storage unit and a store may carry a quadratic marginal cost --- docs/examples/pypsa.md | 180 ++++++++++++++++-- examples/pypsa.yaml | 58 +++++- examples/references/pypsa/references.json | 35 ++++ .../rung_36_quadratic_storage_process.py | 30 +++ examples/symbols/pypsa.yaml | 3 + 5 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 examples/references/pypsa/rung_36_quadratic_storage_process.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 31831d22..0a53e0fc 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -87,7 +87,7 @@ def build(): | [`Bus-nodal_balance`](#bus-nodal_balance) | done | a loaded bus with nothing attached: PyPSA refuses, see X2 | | `Bus-meshed-*-nodal_balance` | out | the same balance rows, dealt into linopy containers by how many component columns name a bus — `meshed_thresholds`, an `n.optimize()` keyword defaulting to `[30, 100, 400]`. Same rows, same duals, another name; a modeler whose engine wants the split states it, the file does not (#123) | | [`marginal_cost`](#objective) | done | | -| [`marginal_cost_quadratic`](#objective) | done | rung 10, below; Generator and Link — PyPSA also carries it on storage units and stores, one more term each of the same shape | +| [`marginal_cost_quadratic`](#objective) | done | rungs 10 and 36, below; Generator and Link `p`, Process `p`, StorageUnit `p_dispatch` only, and Store net `p` | | `objective_constant` | split | an objective shift, compared net of `n._objective_constant` — rungs 11 and 13 carry a nonzero one, `21915277.52` and `160.0`, so the netting is under test | @@ -817,9 +817,9 @@ def build(): A marginal cost quadratic in output: PyPSA's `marginal_cost_quadratic`, one squared term per component in the objective, each snapshot weighted by the hours -it stands for. Generator and Link carry it here; PyPSA also carries it on -storage units and stores, one more term each of the same shape. A plain run -feeds zero, so the term vanishes and the objective stays linear. +it stands for. Generator and Link carry it here; rung 36 puts it on a process, +a storage unit and a store. A plain run feeds zero, so the term vanishes and +the objective stays linear. | PyPSA | status | note | | --- | --- | --- | @@ -1295,15 +1295,17 @@ scenario as well. Capacity does not, because it is chosen once before the future is known. The operating cost is the expectation over the scenarios' weights. A risk preference adds the CVaR (conditional value at risk) rows: an excess per scenario and the tail's average, blended into the objective at -`omega`. +`omega`. PyPSA builds neither row without a risk preference +(`optimize.py:458`). The file builds them only where `omega` is positive, so a +plain run, and a risk preference with `omega = 0`, has none. | PyPSA | status | note | | --- | --- | --- | | [`Generator-p`, `Link-p`](#variable-domains) | done | over `scenario`; `Generator-p_nom` is not — chosen once | | [`Generator-fix-p-*`, `-ext-p-*`, `Link-fix-p-*`, `Bus-nodal_balance`](#generator-fix-p-lower) | done | rungs 1 and 3, over `scenario` | | [`CVaR-a`, `CVaR-theta`, `CVaR`](#variable-domains) | done | | -| [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension | -| [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep | +| [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension; none where `omega` is zero | +| [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep; none where `omega` is zero | | [objective](#objective) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | @@ -3026,6 +3028,68 @@ def build(): +### Rung 36 — quadratic costs on a process and on storage + +PyPSA's `marginal_cost_quadratic` on the three other components that carry it +(`variables.csv:22`, `:30`, `:33`): a process pays on its internal power `p`, a +storage unit on `p_dispatch` only, and a store on its net `p`, so charging +costs as much as delivering. Each term is the square times the cost, weighted +as the linear term is (`optimize.py:317-334`). A plain run feeds zero, so the +terms vanish. + +The rung puts a quadratic cost on a process, a storage unit, with a cost that +changes per snapshot, and a store. With the same network and no quadratic +cost, PyPSA solves to `17641.666666666668`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`marginal_cost_quadratic`](#objective) on Process, StorageUnit and Store | done | degree 2 in the objective; `p_store` is not charged | +| a quadratic cost under a risk preference | refused, as PyPSA | assumed: [`Generator_marginal_cost_quadratic_without_risk_preference`](#generator_marginal_cost_quadratic_without_risk_preference), and the `Link`, `Process`, `StorageUnit` and `Store` ones | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `19185.241281403858`, 84 rows. + +
+The network, as PyPSA code + +`rung_36_quadratic_storage_process.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 36: quadratic costs on a process, a storage unit and a store — the storage unit pays on dispatch only, the store on its net power both ways.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'hub') + n.add('Process', 'conv36', bus0='north', bus1='hub', p_nom=60, marginal_cost=1, marginal_cost_quadratic=0.05) + n.add( + 'StorageUnit', + 'battery36', + bus='south', + p_nom=20, + max_hours=4, + state_of_charge_initial=40, + marginal_cost=0.5, + marginal_cost_quadratic=[0.2, 0.1, 0.3, 0.1], + ) + n.add('Store', 'tank36', bus='hub', e_nom=60, e_initial=20, marginal_cost_quadratic=0.4) + n.add('Load', 'hub_load', bus='hub', p_set=[20, 45, 30, 50]) + n.add('Load', 'peak36', bus='south', p_set=[10, 40, 20, 60]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3036,6 +3100,7 @@ where it should live — language, data prep, or harness — is one open questio | PyPSA raises | on | here | note | | -------------------------------------------- | ------------------------------------------------- | ----------------------- | ---- | +| `ValueError`, `optimize.py:467-474` | a nonzero `marginal_cost_quadratic` on any Generator, Link, Process, StorageUnit or Store under a risk preference | assumed where `omega > 0`: [`Generator_marginal_cost_quadratic_without_risk_preference`](#generator_marginal_cost_quadratic_without_risk_preference), and the `Link`, `Process`, `StorageUnit` and `Store` ones. The file cannot tell no risk preference from `omega = 0`, which PyPSA also refuses | | | `ValueError`, `constraints.py:1850` | fixed modular `p_nom` not a multiple of `p_nom_mod` | a fractional module cap | X1 | | `ValueError`, `constraints.py:1557` | load on a bus with nothing attached | row not built, unserved | X2 | | `ValueError`, `optimize.py:436` | no component carries a cost | feasibility problem | X3 | @@ -3055,7 +3120,7 @@ concatenation of the regime blocks, `p0`/`p1` derived from `Link-p`. ## The file -A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. +A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows, which stand only where that share is positive. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. #### Sets @@ -3153,6 +3218,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{d}^{z}`$ | `Process_output_delay` over $`\mathcal{R}`$ — snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that transfers at once | | $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the horizon's end — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at the first snapshots is lost | | $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | +| $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | | $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit | | $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit | | $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\mathcal{T} \times \mathcal{J}`$ — a given internal power schedule; a process without one has no row here | @@ -3246,6 +3312,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{open}`$ | `StorageUnit_opens_late` over $`\mathcal{T} \times \mathcal{S}`$ — whether a snapshot is the first a storage unit stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every unit stands throughout | | $`\mathrm{idle}`$ | `StorageUnit_inactive_snapshots` over $`\mathcal{S}`$ — how many snapshots a storage unit does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic unit reaches back this many snapshots further, so it closes on the last snapshot it stands in | | $`\mathrm{c}^{h}`$ | `StorageUnit_marginal_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of dispatch | +| $`\mathrm{c}^{h,(2)}`$ | `StorageUnit_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{S}`$ — cost of the square of one unit of dispatch; storing is not charged | | $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | | $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | | $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given net dispatch schedule; a unit without one has no row here | @@ -3262,6 +3329,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{open}^{e}`$ | `Store_opens_late` over $`\mathcal{T} \times \mathcal{V}`$ — whether a snapshot is the first a store stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every store stands throughout | | $`\mathrm{idle}^{e}`$ | `Store_inactive_snapshots` over $`\mathcal{V}`$ — how many snapshots a store does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic store reaches back this many snapshots further, so it closes on the last snapshot it stands in | | $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | +| $`\mathrm{c}^{q,(2)}`$ | `Store_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{V}`$ — cost of the square of the net power delivered, so charging costs as much as delivering | | $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | | $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | | $`\mathrm{s}^{\mathrm{nom}}`$ | `Line_s_nom` over $`\mathcal{K}`$ — nominal apparent power | @@ -7408,11 +7476,12 @@ Carrier_growth_limit: CVaR_excess: description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" dims: [scenario] + where: CVaR_omega > 0 expression: CVaR_a - scenario_opex + CVaR_theta >= 0 ``` ```math -a_{\xi} - \mathit{scenario\_opex}_{\xi} + \theta \ge 0 \qquad \forall\, \xi \in \Xi +a_{\xi} - \mathit{scenario\_opex}_{\xi} + \theta \ge 0 \qquad \forall\, \xi \in \Xi \,:\, \omega > 0 ``` ### `CVaR-def` @@ -7423,11 +7492,12 @@ a_{\xi} - \mathit{scenario\_opex}_{\xi} + \theta \ge 0 \qquad \forall\, \xi \in CVaR_def: description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" dims: [] + where: CVaR_omega > 0 expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR ``` ```math -\theta + \mathrm{v} \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot a_{\xi} \right) \le CVaR +\theta + \mathrm{v} \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot a_{\xi} \right) \le CVaR \qquad \text{where } \omega > 0 ``` ### `Generator_previous_status` @@ -8282,10 +8352,13 @@ scenario_opex: + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Link_p * Link_p * Link_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Process_p * Process_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_p * Process_p * Process_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(StorageUnit_p_dispatch * StorageUnit_p_dispatch * StorageUnit_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(Store_p * Store_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + + sum(sum(Store_p * Store_p * Store_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) @@ -8299,7 +8372,7 @@ scenario_opex: ``` ```math -\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Carrier_additions` @@ -9138,6 +9211,91 @@ Store_operational_limit_carried_over_has_unit_years: ```math \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{b}^{e}_{i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \mathrm{in}_{i,t} ``` + +### `Generator_marginal_cost_quadratic_without_risk_preference` + +```yaml +Generator_marginal_cost_quadratic_without_risk_preference: + holds: "Generator_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive +``` + +```math +\mathrm{c}^{(2)}_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \omega > 0 +``` + +### `Link_marginal_cost_quadratic_without_risk_preference` + +```yaml +Link_marginal_cost_quadratic_without_risk_preference: + holds: "Link_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive +``` + +```math +\mathrm{c}^{f,(2)}_{t,l} = 0 \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \omega > 0 +``` + +### `Process_marginal_cost_quadratic_without_risk_preference` + +```yaml +Process_marginal_cost_quadratic_without_risk_preference: + holds: "Process_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive +``` + +```math +\mathrm{c}^{z,(2)}_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \omega > 0 +``` + +### `StorageUnit_marginal_cost_quadratic_without_risk_preference` + +```yaml +StorageUnit_marginal_cost_quadratic_without_risk_preference: + holds: "StorageUnit_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive +``` + +```math +\mathrm{c}^{h,(2)}_{t,s} = 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \omega > 0 +``` + +### `Store_marginal_cost_quadratic_without_risk_preference` + +```yaml +Store_marginal_cost_quadratic_without_risk_preference: + holds: "Store_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive +``` + +```math +\mathrm{c}^{q,(2)}_{t,v} = 0 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \omega > 0 +``` Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 0b26fd25..bd82a878 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -8,8 +8,8 @@ description: >- chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' - weights, with a share priced at the tail through the CVaR rows. A plain run - feeds one scenario, one period, all-active masks and unit weights, and the + weights, with a share priced at the tail through the CVaR rows, which stand + only where that share is positive. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the @@ -364,6 +364,9 @@ parameters: Process_marginal_cost: description: cost of one unit of internal power dims: [snapshot, process] + Process_marginal_cost_quadratic: + description: cost of the square of one unit of internal power + dims: [snapshot, process] Process_ramp_limit_up: description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit dims: [process] @@ -703,6 +706,9 @@ parameters: StorageUnit_marginal_cost: description: cost of one unit of dispatch dims: [snapshot, storage_unit] + StorageUnit_marginal_cost_quadratic: + description: cost of the square of one unit of dispatch; storing is not charged + dims: [snapshot, storage_unit] StorageUnit_marginal_cost_storage: description: cost of one unit of charge held over one snapshot dims: [snapshot, storage_unit] @@ -770,6 +776,9 @@ parameters: Store_marginal_cost: description: cost of one unit of power delivered dims: [snapshot, store] + Store_marginal_cost_quadratic: + description: cost of the square of the net power delivered, so charging costs as much as delivering + dims: [snapshot, store] Store_marginal_cost_storage: description: cost of one unit of energy held over one snapshot dims: [snapshot, store] @@ -1975,10 +1984,13 @@ expressions: + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Link_p * Link_p * Link_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + sum(sum(Process_p * Process_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_p * Process_p * Process_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + sum(sum(StorageUnit_p_dispatch * StorageUnit_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + + sum(sum(StorageUnit_p_dispatch * StorageUnit_p_dispatch * StorageUnit_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(StorageUnit_spill * StorageUnit_spill_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=storage_unit), over=snapshot) + sum(sum(Store_p * Store_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + + sum(sum(Store_p * Store_p * Store_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) @@ -3540,10 +3552,12 @@ constraints: CVaR_excess: description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" dims: [scenario] + where: CVaR_omega > 0 expression: CVaR_a - scenario_opex + CVaR_theta >= 0 CVaR_def: description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" dims: [] + where: CVaR_omega > 0 expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR assumptions: @@ -3752,6 +3766,46 @@ assumptions: description: >- the same for an `operational_limit` row, over the periods it counts — PyPSA refuses it (`global_constraints.py:695`) + Generator_marginal_cost_quadratic_without_risk_preference: + holds: "Generator_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive + Link_marginal_cost_quadratic_without_risk_preference: + holds: "Link_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive + Process_marginal_cost_quadratic_without_risk_preference: + holds: "Process_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive + StorageUnit_marginal_cost_quadratic_without_risk_preference: + holds: "StorageUnit_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive + Store_marginal_cost_quadratic_without_risk_preference: + holds: "Store_marginal_cost_quadratic == 0" + where: "CVaR_omega > 0" + description: >- + a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA + refuses quadratic costs under any risk preference + (`optimize.py:467-474`). The spec cannot tell no risk preference from + one with `omega = 0`, so it refuses only where `omega` is positive objective: sense: minimize diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index c968e575..9a7ed1c2 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1438,5 +1438,40 @@ "Store-fix-e-lower": 8, "Store-fix-e-upper": 8 } + }, + "rung_36_quadratic_storage_process": { + "columns": { + "Generator-p": 8, + "Link-p": 4, + "Process-p": 4, + "StorageUnit-p_dispatch": 4, + "StorageUnit-p_store": 4, + "StorageUnit-state_of_charge": 4, + "Store-e": 4, + "Store-p": 4 + }, + "global_constraints": {}, + "objective": 19185.241281403858, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "Process-fix-p-lower": 4, + "Process-fix-p-upper": 4, + "StorageUnit-energy_balance": 4, + "StorageUnit-fix-p_dispatch-lower": 4, + "StorageUnit-fix-p_dispatch-upper": 4, + "StorageUnit-fix-p_store-lower": 4, + "StorageUnit-fix-p_store-upper": 4, + "StorageUnit-fix-state_of_charge-lower": 4, + "StorageUnit-fix-state_of_charge-upper": 4, + "Store-energy_balance": 4, + "Store-fix-e-lower": 4, + "Store-fix-e-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_36_quadratic_storage_process.py b/examples/references/pypsa/rung_36_quadratic_storage_process.py new file mode 100644 index 00000000..b72f5c7a --- /dev/null +++ b/examples/references/pypsa/rung_36_quadratic_storage_process.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 36: quadratic costs on a process, a storage unit and a store — the storage unit pays on dispatch only, the store on its net power both ways.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Bus', 'hub') + n.add('Process', 'conv36', bus0='north', bus1='hub', p_nom=60, marginal_cost=1, marginal_cost_quadratic=0.05) + n.add( + 'StorageUnit', + 'battery36', + bus='south', + p_nom=20, + max_hours=4, + state_of_charge_initial=40, + marginal_cost=0.5, + marginal_cost_quadratic=[0.2, 0.1, 0.3, 0.1], + ) + n.add('Store', 'tank36', bus='hub', e_nom=60, e_initial=20, marginal_cost_quadratic=0.4) + n.add('Load', 'hub_load', bus='hub', p_set=[20, 45, 30, 50]) + n.add('Load', 'peak36', bus='south', p_set=[10, 40, 20, 60]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 8b1299a9..ea93bb56 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -131,6 +131,7 @@ names: Process_output_delay: '\mathrm{d}^{z}' Process_output_cyclic_delay: '\mathrm{cyc}^{z}' Process_marginal_cost: '\mathrm{c}^{z}' + Process_marginal_cost_quadratic: '\mathrm{c}^{z,(2)}' Process_ramp_limit_up: '\mathrm{ru}^{z}' Process_ramp_limit_down: '\mathrm{rd}^{z}' Process_p_set: '\mathrm{z}^{\mathrm{set}}' @@ -261,6 +262,7 @@ names: StorageUnit_opens_late: '\mathrm{open}' StorageUnit_inactive_snapshots: '\mathrm{idle}' StorageUnit_marginal_cost: '\mathrm{c}^{h}' + StorageUnit_marginal_cost_quadratic: '\mathrm{c}^{h,(2)}' StorageUnit_marginal_cost_storage: '\mathrm{c}^{\mathrm{soc}}' StorageUnit_spill_cost: '\mathrm{c}^{\mathrm{spill}}' StorageUnit_p_set: '\mathrm{h}^{\mathrm{set}}' @@ -280,6 +282,7 @@ names: Store_opens_late: '\mathrm{open}^{e}' Store_inactive_snapshots: '\mathrm{idle}^{e}' Store_marginal_cost: '\mathrm{c}^{q}' + Store_marginal_cost_quadratic: '\mathrm{c}^{q,(2)}' Store_marginal_cost_storage: '\mathrm{c}^{e}' Store_e_set: '\mathrm{e}^{\mathrm{set}}' scenario_weight: '\pi' From a0777a4a3357a103398e7544f17791b14b3f4b60 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 14:08:55 +0200 Subject: [PATCH 24/46] docs(pypsa): a store's power and a storage unit's dispatch and charging may each be pinned to a schedule --- docs/examples/pypsa.md | 113 +++++++++++++++++- examples/pypsa.yaml | 24 ++++ examples/references/pypsa/references.json | 35 ++++++ .../pypsa/rung_37_fixed_storage_dispatch.py | 29 +++++ examples/symbols/pypsa.yaml | 3 + 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 examples/references/pypsa/rung_37_fixed_storage_dispatch.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 0a53e0fc..481c26c9 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -128,7 +128,7 @@ def build(): | [`StorageUnit-fix-*`](#storageunit-fix-p_dispatch-lower), [`Store-fix-e-*`](#store-fix-e-lower) | done | | | [`StorageUnit-energy_balance`](#storageunit-energy_balance) | done | the charge carried into a snapshot is a cased quantity — cyclic, opening, carried; `(1-loss)**eh` is prep | | [`Store-energy_balance`](#store-energy_balance) | done | same | -| [`StorageUnit-p_set`](#storageunit-p_set), [`{c}-{attr}_set`](#generator-p_set) | done | `Generator-p_set`, `Link-p_set`, `StorageUnit-state_of_charge_set`, `Store-e_set`, `Line-s_set` | +| [`StorageUnit-p_set`](#storageunit-p_set), [`{c}-{attr}_set`](#generator-p_set) | done | `Generator-p_set`, `Link-p_set`, `StorageUnit-state_of_charge_set`, `Store-e_set`, `Line-s_set`; `Store-p_set`, `StorageUnit-p_dispatch_set`, `-p_store_set` in rung 37 | | [`marginal_cost_storage`, `spill_cost`](#objective) | done | | @@ -3090,6 +3090,66 @@ def build(): +### Rung 37 — storage dispatch pinned to a schedule + +PyPSA pins a store's power delivered to `p_set`, and a storage unit's dispatch +and charging to `p_dispatch_set` and `p_store_set`, each on its own +(`optimize.py:846`, `:851`; `constraints.py:1961-2019`). A row stands only +where a value is given and the storage is active. A plain run gives no value, +so no row stands. + +The rung pins a store to deliver 10 in the first snapshot and to take 5 in the +last, and pins a storage unit's dispatch in the first snapshot and its +charging in the second. With the same network and no pins, PyPSA solves to +`5811.111111111111`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Store-p_set`](#store-p_set), [`StorageUnit-p_dispatch_set`](#storageunit-p_dispatch_set), [`StorageUnit-p_store_set`](#storageunit-p_store_set) | done | `where:` a value is given and the storage is active | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `6395.833333333333`, 76 rows. + +
+The network, as PyPSA code + +`rung_37_fixed_storage_dispatch.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 37: storage dispatch pinned — a store's power delivered, and a storage unit's dispatch and charging, each on its own schedule.""" + +from __future__ import annotations + +from math import nan + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.generators_t.marginal_cost['gas'] = [15, 60, 15, 60] + n.add('Store', 'tank37', bus='south', e_nom=40, e_initial=20, p_set=[10, nan, nan, -5]) + n.add( + 'StorageUnit', + 'battery37', + bus='south', + p_nom=20, + max_hours=2, + state_of_charge_initial=10, + p_dispatch_set=[6, nan, nan, nan], + p_store_set=[nan, 4, nan, nan], + ) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3316,6 +3376,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | | $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | | $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given net dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{+,\mathrm{set}}`$ | `StorageUnit_p_dispatch_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{-,\mathrm{set}}`$ | `StorageUnit_p_store_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given charging schedule; a unit without one has no row here | | $`\mathrm{soc}^{\mathrm{set}}`$ | `StorageUnit_state_of_charge_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given charge schedule; a unit without one has no row here | | $`\mathrm{e}^{\mathrm{nom}}`$ | `Store_e_nom` over $`\mathcal{V}`$ — nominal energy capacity | | $`\mathrm{ext}^{e}`$ | `Store_e_nom_extendable` over $`\mathcal{V}`$ — whether the nominal energy capacity is a decision | @@ -3332,6 +3394,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{c}^{q,(2)}`$ | `Store_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{V}`$ — cost of the square of the net power delivered, so charging costs as much as delivering | | $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | | $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | +| $`\mathrm{q}^{\mathrm{set}}`$ | `Store_p_set` over $`\mathcal{T} \times \mathcal{V}`$ — a given schedule of power delivered; a store without one has no row here | | $`\mathrm{s}^{\mathrm{nom}}`$ | `Line_s_nom` over $`\mathcal{K}`$ — nominal apparent power | | $`\mathrm{ext}^{s}`$ | `Line_s_nom_extendable` over $`\mathcal{K}`$ — whether the nominal apparent power is a decision | | $`\overline{\mathrm{s}}`$ | `Line_s_max_pu` over $`\mathcal{T} \times \mathcal{K}`$ — most flow either way, per unit of nominal apparent power | @@ -7134,6 +7197,38 @@ StorageUnit_p_set: h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} = \mathrm{h}^{\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} ``` +### `StorageUnit-p_dispatch_set` + +`StorageUnit_p_dispatch_set` + +```yaml +StorageUnit_p_dispatch_set: + description: "`StorageUnit-p_dispatch_set` — dispatch pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_dispatch_set AND StorageUnit_active + expression: StorageUnit_p_dispatch == StorageUnit_p_dispatch_set +``` + +```math +h^{+}_{\xi,t,s} = \mathrm{h}^{+,\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{+,\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} +``` + +### `StorageUnit-p_store_set` + +`StorageUnit_p_store_set` + +```yaml +StorageUnit_p_store_set: + description: "`StorageUnit-p_store_set` — charging pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_store_set AND StorageUnit_active + expression: StorageUnit_p_store == StorageUnit_p_store_set +``` + +```math +h^{-}_{\xi,t,s} = \mathrm{h}^{-,\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{-,\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} +``` + ### `StorageUnit-state_of_charge_set` `StorageUnit_state_of_charge_set` @@ -7166,6 +7261,22 @@ Store_e_set: e_{\xi,t,v} = \mathrm{e}^{\mathrm{set}}_{t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{e}^{\mathrm{set}}_{t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} ``` +### `Store-p_set` + +`Store_p_set` + +```yaml +Store_p_set: + description: "`Store-p_set` — power delivered pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, store] + where: Store_p_set AND Store_active + expression: Store_p == Store_p_set +``` + +```math +q_{\xi,t,v} = \mathrm{q}^{\mathrm{set}}_{t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{q}^{\mathrm{set}}_{t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} +``` + ### `primary_energy` `GlobalConstraint_primary_energy_ub` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index bd82a878..0f7915f4 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -718,6 +718,12 @@ parameters: StorageUnit_p_set: description: a given net dispatch schedule; a unit without one has no row here dims: [snapshot, storage_unit] + StorageUnit_p_dispatch_set: + description: a given dispatch schedule; a unit without one has no row here + dims: [snapshot, storage_unit] + StorageUnit_p_store_set: + description: a given charging schedule; a unit without one has no row here + dims: [snapshot, storage_unit] StorageUnit_state_of_charge_set: description: a given charge schedule; a unit without one has no row here dims: [snapshot, storage_unit] @@ -785,6 +791,9 @@ parameters: Store_e_set: description: a given energy schedule; a store without one has no row here dims: [snapshot, store] + Store_p_set: + description: a given schedule of power delivered; a store without one has no row here + dims: [snapshot, store] Line_s_nom: description: nominal apparent power dims: [line] @@ -3424,6 +3433,16 @@ constraints: dims: [scenario, snapshot, storage_unit] where: StorageUnit_p_set AND StorageUnit_active expression: StorageUnit_p_dispatch - StorageUnit_p_store == StorageUnit_p_set + StorageUnit_p_dispatch_set: + description: "`StorageUnit-p_dispatch_set` — dispatch pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_dispatch_set AND StorageUnit_active + expression: StorageUnit_p_dispatch == StorageUnit_p_dispatch_set + StorageUnit_p_store_set: + description: "`StorageUnit-p_store_set` — charging pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, storage_unit] + where: StorageUnit_p_store_set AND StorageUnit_active + expression: StorageUnit_p_store == StorageUnit_p_store_set StorageUnit_state_of_charge_set: description: "`StorageUnit-state_of_charge_set` — charge pinned to the given schedule, wherever one is given" dims: [scenario, snapshot, storage_unit] @@ -3434,6 +3453,11 @@ constraints: dims: [scenario, snapshot, store] where: Store_e_set AND Store_active expression: Store_e == Store_e_set + Store_p_set: + description: "`Store-p_set` — power delivered pinned to the given schedule, wherever one is given" + dims: [scenario, snapshot, store] + where: Store_p_set AND Store_active + expression: Store_p == Store_p_set GlobalConstraint_primary_energy_ub: description: "`primary_energy` — its total, at most its constant" dims: [scenario, global_constraint] diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 9a7ed1c2..145e074b 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1473,5 +1473,40 @@ "Store-fix-e-lower": 4, "Store-fix-e-upper": 4 } + }, + "rung_37_fixed_storage_dispatch": { + "columns": { + "Generator-p": 8, + "Link-p": 4, + "StorageUnit-p_dispatch": 4, + "StorageUnit-p_store": 4, + "StorageUnit-state_of_charge": 4, + "Store-e": 4, + "Store-p": 4 + }, + "global_constraints": {}, + "objective": 6395.833333333333, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "StorageUnit-energy_balance": 4, + "StorageUnit-fix-p_dispatch-lower": 4, + "StorageUnit-fix-p_dispatch-upper": 4, + "StorageUnit-fix-p_store-lower": 4, + "StorageUnit-fix-p_store-upper": 4, + "StorageUnit-fix-state_of_charge-lower": 4, + "StorageUnit-fix-state_of_charge-upper": 4, + "StorageUnit-p_dispatch_set": 1, + "StorageUnit-p_store_set": 1, + "Store-energy_balance": 4, + "Store-fix-e-lower": 4, + "Store-fix-e-upper": 4, + "Store-p_set": 2 + } } } diff --git a/examples/references/pypsa/rung_37_fixed_storage_dispatch.py b/examples/references/pypsa/rung_37_fixed_storage_dispatch.py new file mode 100644 index 00000000..f4fa6bf4 --- /dev/null +++ b/examples/references/pypsa/rung_37_fixed_storage_dispatch.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 37: storage dispatch pinned — a store's power delivered, and a storage unit's dispatch and charging, each on its own schedule.""" + +from __future__ import annotations + +from math import nan + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.generators_t.marginal_cost['gas'] = [15, 60, 15, 60] + n.add('Store', 'tank37', bus='south', e_nom=40, e_initial=20, p_set=[10, nan, nan, -5]) + n.add( + 'StorageUnit', + 'battery37', + bus='south', + p_nom=20, + max_hours=2, + state_of_charge_initial=10, + p_dispatch_set=[6, nan, nan, nan], + p_store_set=[nan, 4, nan, nan], + ) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index ea93bb56..6852461b 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -266,6 +266,8 @@ names: StorageUnit_marginal_cost_storage: '\mathrm{c}^{\mathrm{soc}}' StorageUnit_spill_cost: '\mathrm{c}^{\mathrm{spill}}' StorageUnit_p_set: '\mathrm{h}^{\mathrm{set}}' + StorageUnit_p_dispatch_set: '\mathrm{h}^{+,\mathrm{set}}' + StorageUnit_p_store_set: '\mathrm{h}^{-,\mathrm{set}}' StorageUnit_state_of_charge_set: '\mathrm{soc}^{\mathrm{set}}' Store_e: "e" Store_p: "q" @@ -285,6 +287,7 @@ names: Store_marginal_cost_quadratic: '\mathrm{c}^{q,(2)}' Store_marginal_cost_storage: '\mathrm{c}^{e}' Store_e_set: '\mathrm{e}^{\mathrm{set}}' + Store_p_set: '\mathrm{q}^{\mathrm{set}}' scenario_weight: '\pi' CVaR_omega: '\omega' CVaR_inv_tail: '\mathrm{v}' From d96464991fae6617ea803ba1c33a7fc43fe5454b Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 14:16:43 +0200 Subject: [PATCH 25/46] docs(pypsa): a link's and a process's delay applies within each investment period --- docs/examples/pypsa.md | 130 +++++++++++++++--- examples/pypsa.yaml | 39 +++--- examples/references/pypsa/references.json | 20 +++ .../pypsa/rung_38_delay_per_period.py | 53 +++++++ 4 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 examples/references/pypsa/rung_38_delay_per_period.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 481c26c9..251e3930 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1363,6 +1363,7 @@ own dimensions. | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) for storage built in a later period or retired early | done | rung 32 | | [`primary_energy`](#primary_energy), [`operational_limit`](#operational_limit) for one investment period, weighted by period years | done | rung 35 | +| [link and process `delay`, `cyclic_delay`](#bus-nodal_balance) per investment period | done | rung 38 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `12747.19109626398`, 80 rows. @@ -3150,6 +3151,96 @@ def build(): +### Rung 38 — delays per investment period + +`n.optimize(multi_investment_periods=True)` with delayed ports. PyPSA applies +a link's or a process's `delay` in each investment period on its own +(`constraints.py:1324-1332`; `multiports.py:212-219`). A `cyclic_delay` port +wraps from the end of its own period. A port that is not cyclic loses the flow +still in transit at the first snapshots of every period. PyPSA measures the +delay in `generators` weighting per period and rounds it down to a snapshot +start (`multiports.py:106-123`). Scenarios do not change the source snapshot. +`Link_output_arrival` and `Process_output_arrival` therefore shift with +`by=snapshot_period, within=period`. A plain run has one period, so the shift +is the flat one. + +The rung builds a link that delays by two and wraps, and a process that +delays by one and does not wrap, on two periods of four snapshots. The earlier +file shifted over the flat horizon, so 2030 read the flow sent in 2020. With +that flat shift patched into PyPSA's source index, the network solves to +`10543.75` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [link and process `delay`, `cyclic_delay`](#bus-nodal_balance) per investment period | done | `shift(offset=delay, by=snapshot_period, within=period)`; `edge='wrap'` closes each period, `edge=0` vacates each period's first snapshots | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `12918.75`, 104 rows. + +
+The network, as PyPSA code + +`rung_38_delay_per_period.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 38: delays per investment period — a link that wraps its delayed flow within each period, and a process that loses what is still in transit at each period's start.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + +#: The `generators` weighting is uniform, as on rung 16, so a delay of `n` is a +#: shift of exactly `n` positions. The `objective` column stays non-uniform. +WEIGHTINGS = {'objective': [2.0, 1.5, 2.5, 3.0, 2.0, 1.5, 2.5, 3.0], 'generators': [1.0] * 8} + +#: Demand differs from snapshot to snapshot, so which snapshot a delayed flow is +#: read from changes what it costs. +DEMAND = [20.0, 15.0, 25.0, 10.0, 30.0, 35.0, 5.0, 40.0] + + +def build(): + """A whole network, not the spine: eight snapshots over two periods, a source, a delayed link and a delayed process. + + ``pipe_wrap`` delays by two snapshots and wraps cyclically, so the first two + snapshots of each period read the last two of that same period, never the + other period. ``conv_lose`` delays by one and does not wrap, so the first + snapshot of each period, 2030 included, receives nothing and its demand falls + to the backup. The source is capped, so where each delayed flow is read from + decides how much of the backup runs. + """ + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + for column, values in WEIGHTINGS.items(): + n.snapshot_weightings[column] = values + n.add('Bus', ['source', 'sink_wrap', 'sink_lose']) + n.add('Generator', 'spring38', bus='source', p_nom=60, marginal_cost=5) + n.add('Generator', 'backup_wrap38', bus='sink_wrap', p_nom=200, marginal_cost=100) + n.add('Generator', 'backup_lose38', bus='sink_lose', p_nom=200, marginal_cost=100) + n.add('Link', 'pipe_wrap', bus0='source', bus1='sink_wrap', p_nom=30, delay=2, cyclic_delay=True) + n.add('Process', 'conv_lose', bus0='source', bus1='sink_lose', p_nom=30, delay1=1, cyclic_delay1=False) + n.add('Load', 'load_wrap', bus='sink_wrap', p_set=DEMAND) + n.add('Load', 'load_lose', bus='sink_lose', p_set=DEMAND) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3247,7 +3338,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | | $`\eta`$ | `Link_efficiency` over $`\mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | | $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once | -| $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the horizon's end — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at the first snapshots is lost | +| $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at each period's first snapshots is lost | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | | $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | | $`\mathrm{com}^{f}`$ | `Link_committable` over $`\mathcal{L}`$ — whether flow is gated by an on/off status decision | @@ -3276,7 +3367,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overline{\mathrm{z}}`$ | `Process_p_max_pu` over $`\mathcal{T} \times \mathcal{J}`$ — most internal power, per unit of nominal power | | $`\alpha`$ | `Process_rate` over $`\mathcal{R}`$ — the energy a port draws or delivers per unit of internal power, PyPSA's `rate0`, `rate1`, … read long — negative where the port withdraws, positive where it injects; a link is a process whose `bus0` rate is minus one and whose output rates are its efficiencies | | $`\mathrm{d}^{z}`$ | `Process_output_delay` over $`\mathcal{R}`$ — snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that transfers at once | -| $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the horizon's end — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at the first snapshots is lost | +| $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost | | $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | | $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | | $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit | @@ -3537,8 +3628,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\Delta^{z,-}`$ | `Process_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may lower internal power between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial charge, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A unit built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon | | $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial energy, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A store built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon | -| $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed flow wraps from the horizon's end, and where it is not the flow still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | -| $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay`; where the port is `cyclic_delay` the delayed transfer wraps from the horizon's end, and where it is not the energy still in transit at the first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | +| $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay` within its investment period; where the port is `cyclic_delay` the delayed flow wraps from the period's end, and where it is not the flow still in transit at the period's first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | +| $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay` within its investment period; where the port is `cyclic_delay` the delayed transfer wraps from the period's end, and where it is not the energy still in transit at the period's first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | | $`\mathit{w}^{\mathrm{gc}}`$ | `GlobalConstraint_energy_weight` over $`\mathcal{I} \times \mathcal{T}`$ — what one unit of power at a snapshot counts for in a row — the generator weighting times the years of the snapshot's period, where the row counts the snapshot, and nothing where it does not | | $`\mathit{last}`$ | `GlobalConstraint_snapshot_closes` over $`\mathcal{I} \times \mathcal{T}`$ — one at the last snapshot a row counts, and zero elsewhere | | $`\mathit{w}^{h}`$ | `StorageUnit_closing_weight` over $`\mathcal{I} \times \mathcal{T} \times \mathcal{S}`$ — what the charge a unit holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the unit reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere | @@ -8244,21 +8335,21 @@ Store_energy_carried_in: Link_output_arrival: description: >- what a link delivers to an output port at a snapshot — its flow after the - port's efficiency, delayed by the port's `delay`; where the port is - `cyclic_delay` the delayed flow wraps from the horizon's end, and where it - is not the flow still in transit at the first snapshots is lost. A port - that does not delay (`delay` zero) delivers its flow unshifted, cyclic or - not + port's efficiency, delayed by the port's `delay` within its investment + period; where the port is `cyclic_delay` the delayed flow wraps from the + period's end, and where it is not the flow still in transit at the + period's first snapshots is lost. A port that does not delay (`delay` + zero) delivers its flow unshifted, cyclic or not dims: [scenario, snapshot, link_output] cases: wrapping: when: Link_output_cyclic_delay - expression: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge='wrap') - otherwise: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge=0) + expression: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge='wrap', by=snapshot_period, within=period) + otherwise: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge=0, by=snapshot_period, within=period) ``` ```math -\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} +\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} ``` ### `Process_output_arrival` @@ -8267,20 +8358,21 @@ Link_output_arrival: Process_output_arrival: description: >- what a process transfers at a port at a snapshot — its internal power - times the port's rate, delayed by the port's `delay`; where the port is - `cyclic_delay` the delayed transfer wraps from the horizon's end, and where - it is not the energy still in transit at the first snapshots is lost. A - port that does not delay (`delay` zero) transfers at once, cyclic or not + times the port's rate, delayed by the port's `delay` within its + investment period; where the port is `cyclic_delay` the delayed transfer + wraps from the period's end, and where it is not the energy still in + transit at the period's first snapshots is lost. A port that does not + delay (`delay` zero) transfers at once, cyclic or not dims: [scenario, snapshot, process_output] cases: wrapping: when: Process_output_cyclic_delay - expression: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge='wrap') - otherwise: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge=0) + expression: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge='wrap', by=snapshot_period, within=period) + otherwise: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge=0, by=snapshot_period, within=period) ``` ```math -\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} +\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} ``` ### `GlobalConstraint_energy_weight` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 0f7915f4..08010675 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -224,9 +224,9 @@ parameters: dtype: int Link_output_cyclic_delay: description: >- - whether a delayed port's flow wraps from the horizon's end — PyPSA's - `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in - transit at the first snapshots is lost + whether a delayed port's flow wraps from the end of its investment + period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, + the flow still in transit at each period's first snapshots is lost dims: [link_output] dtype: bool Link_marginal_cost: @@ -356,9 +356,9 @@ parameters: dtype: int Process_output_cyclic_delay: description: >- - whether a delayed port's transfer wraps from the horizon's end — PyPSA's - `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still - in transit at the first snapshots is lost + whether a delayed port's transfer wraps from the end of its investment + period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, + the energy still in transit at each period's first snapshots is lost dims: [process_output] dtype: bool Process_marginal_cost: @@ -1873,30 +1873,31 @@ expressions: Link_output_arrival: description: >- what a link delivers to an output port at a snapshot — its flow after the - port's efficiency, delayed by the port's `delay`; where the port is - `cyclic_delay` the delayed flow wraps from the horizon's end, and where it - is not the flow still in transit at the first snapshots is lost. A port - that does not delay (`delay` zero) delivers its flow unshifted, cyclic or - not + port's efficiency, delayed by the port's `delay` within its investment + period; where the port is `cyclic_delay` the delayed flow wraps from the + period's end, and where it is not the flow still in transit at the + period's first snapshots is lost. A port that does not delay (`delay` + zero) delivers its flow unshifted, cyclic or not dims: [scenario, snapshot, link_output] cases: wrapping: when: Link_output_cyclic_delay - expression: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge='wrap') - otherwise: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge=0) + expression: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge='wrap', by=snapshot_period, within=period) + otherwise: shift(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, along=snapshot, offset=Link_output_delay, edge=0, by=snapshot_period, within=period) Process_output_arrival: description: >- what a process transfers at a port at a snapshot — its internal power - times the port's rate, delayed by the port's `delay`; where the port is - `cyclic_delay` the delayed transfer wraps from the horizon's end, and where - it is not the energy still in transit at the first snapshots is lost. A - port that does not delay (`delay` zero) transfers at once, cyclic or not + times the port's rate, delayed by the port's `delay` within its + investment period; where the port is `cyclic_delay` the delayed transfer + wraps from the period's end, and where it is not the energy still in + transit at the period's first snapshots is lost. A port that does not + delay (`delay` zero) transfers at once, cyclic or not dims: [scenario, snapshot, process_output] cases: wrapping: when: Process_output_cyclic_delay - expression: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge='wrap') - otherwise: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge=0) + expression: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge='wrap', by=snapshot_period, within=period) + otherwise: shift(at(Process_p, by=Process_output_process, over=process, into=process_output) * Process_rate, along=snapshot, offset=Process_output_delay, edge=0, by=snapshot_period, within=period) GlobalConstraint_energy_weight: description: >- what one unit of power at a snapshot counts for in a row — the diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 145e074b..9e2f49c4 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1508,5 +1508,25 @@ "Store-fix-e-upper": 4, "Store-p_set": 2 } + }, + "rung_38_delay_per_period": { + "columns": { + "Generator-p": 24, + "Link-p": 8, + "Process-p": 8 + }, + "global_constraints": {}, + "objective": 12918.75, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 24, + "Generator-fix-p-lower": 24, + "Generator-fix-p-upper": 24, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8, + "Process-fix-p-lower": 8, + "Process-fix-p-upper": 8 + } } } diff --git a/examples/references/pypsa/rung_38_delay_per_period.py b/examples/references/pypsa/rung_38_delay_per_period.py new file mode 100644 index 00000000..38303eda --- /dev/null +++ b/examples/references/pypsa/rung_38_delay_per_period.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 38: delays per investment period — a link that wraps its delayed flow within each period, and a process that loses what is still in transit at each period's start.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + +#: The `generators` weighting is uniform, as on rung 16, so a delay of `n` is a +#: shift of exactly `n` positions. The `objective` column stays non-uniform. +WEIGHTINGS = {'objective': [2.0, 1.5, 2.5, 3.0, 2.0, 1.5, 2.5, 3.0], 'generators': [1.0] * 8} + +#: Demand differs from snapshot to snapshot, so which snapshot a delayed flow is +#: read from changes what it costs. +DEMAND = [20.0, 15.0, 25.0, 10.0, 30.0, 35.0, 5.0, 40.0] + + +def build(): + """A whole network, not the spine: eight snapshots over two periods, a source, a delayed link and a delayed process. + + ``pipe_wrap`` delays by two snapshots and wraps cyclically, so the first two + snapshots of each period read the last two of that same period, never the + other period. ``conv_lose`` delays by one and does not wrap, so the first + snapshot of each period, 2030 included, receives nothing and its demand falls + to the backup. The source is capped, so where each delayed flow is read from + decides how much of the backup runs. + """ + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + for column, values in WEIGHTINGS.items(): + n.snapshot_weightings[column] = values + n.add('Bus', ['source', 'sink_wrap', 'sink_lose']) + n.add('Generator', 'spring38', bus='source', p_nom=60, marginal_cost=5) + n.add('Generator', 'backup_wrap38', bus='sink_wrap', p_nom=200, marginal_cost=100) + n.add('Generator', 'backup_lose38', bus='sink_lose', p_nom=200, marginal_cost=100) + n.add('Link', 'pipe_wrap', bus0='source', bus1='sink_wrap', p_nom=30, delay=2, cyclic_delay=True) + n.add('Process', 'conv_lose', bus0='source', bus1='sink_lose', p_nom=30, delay1=1, cyclic_delay1=False) + n.add('Load', 'load_wrap', bus='sink_wrap', p_set=DEMAND) + n.add('Load', 'load_lose', bus='sink_lose', p_set=DEMAND) + return n From c1fec47db1e119d35a4eda264bcab1aaa2b48b4b Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 14:46:48 +0200 Subject: [PATCH 26/46] docs(pypsa): a negative relative growth adds nothing to a carrier's growth limit, as pypsa clips it at zero --- docs/examples/pypsa.md | 114 +++++++++++++++++- examples/pypsa.yaml | 11 +- examples/references/pypsa/references.json | 23 ++++ .../pypsa/rung_39_negative_relative_growth.py | 57 +++++++++ examples/symbols/pypsa.yaml | 1 + 5 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 examples/references/pypsa/rung_39_negative_relative_growth.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 251e3930..4c07528f 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1359,6 +1359,7 @@ own dimensions. | [`Generator-p`](#variable-domains) | done | where the generator stands in the snapshot's period — `active`, data prep | | [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | | [`Carrier-growth_limit`](#carrier-growth_limit) | done | every extendable component of the carrier, counted in the first period a build stands in; `edge=0` at the first period | +| [`Carrier-growth_limit`](#carrier-growth_limit) with a negative `max_relative_growth` | done | rung 39 | | [objective](#objective) | done | period weight on operation; capacity once per period it stands in | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) for storage built in a later period or retired early | done | rung 32 | @@ -3241,6 +3242,96 @@ def build(): +### Rung 39 — a negative relative growth + +`n.optimize(multi_investment_periods=True)` with a carrier whose +`max_relative_growth` is negative. PyPSA clips the share at zero before it +builds `Carrier-growth_limit` (`global_constraints.py:237`), so a negative +share adds nothing and does not tighten the limit. `Carrier_relative_growth` +states the clip as a case: the given share where it is positive, zero +otherwise. A plain run has one period and builds no growth row. + +The rung builds a battery carrier with `max_growth=20` and +`max_relative_growth=-0.5`, and two stores built in 2020 and 2030. The earlier +file read the share as given, so the 2030 row added half of the 2020 build to +the left side. With that row patched into PyPSA through `extra_functionality`, +the network solves to `9347.5` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [`Carrier-growth_limit`](#carrier-growth_limit), `max_relative_growth.clip(min=0)` | done | `Carrier_relative_growth` is `Carrier_max_relative_growth` where it is positive, `0` otherwise | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `8600.0`, 44 rows. + +
+The network, as PyPSA code + +`rung_39_negative_relative_growth.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 39: a negative relative growth adds nothing to a carrier's growth limit — PyPSA clips it at zero.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: a battery carrier with `max_relative_growth=-0.5` builds in both periods.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', 'grid') + n.add('Carrier', 'solar') + n.add('Carrier', 'gas') + n.add('Carrier', 'battery', max_growth=20, max_relative_growth=-0.5) + n.add('Generator', 'solar', bus='grid', carrier='solar', p_nom=100, marginal_cost=1, p_max_pu=[1, 0, 1, 0]) + n.add('Generator', 'backup', bus='grid', carrier='gas', p_nom=200, marginal_cost=80) + n.add( + 'Store', + 'tank20', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=100, + capital_cost=10, + build_year=2020, + lifetime=30, + ) + n.add( + 'Store', + 'tank30', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=100, + capital_cost=8, + build_year=2030, + lifetime=30, + ) + n.add('Load', 'town', bus='grid', p_set=[40, 60, 40, 80]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3641,6 +3732,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | | $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. Like PyPSA, it sums only the components that carry a carrier attribute, so a transformer, which has none, counts in no carrier | +| $`\mathrm{r}^{+}`$ | `Carrier_relative_growth` over $`\mathcal{I}`$ — the share of the previous period's additions a carrier's growth limit reads — PyPSA's `max_relative_growth` clipped at zero, so a negative share adds nothing and never tightens the limit | | $`\check{s}`$ | `Line_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the flow a line's post-contingency rows read — its flow where it stands, nothing where it does not, since PyPSA builds those rows for every branch of the sub-network in every snapshot | | $`\check{\sigma}`$ | `Transformer_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — the flow a transformer's post-contingency rows read, as a line's | | $`\hat{s}`$ | `Outage_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}^{\mathrm{out}}`$ — the flow an outage takes off its branch — the outaged line's or transformer's flow before it goes out | @@ -7662,12 +7754,12 @@ Carrier_growth_limit: where: Carrier_max_growth expression: >- Carrier_additions - - shift(Carrier_additions, along=period, offset=1, edge=0) * Carrier_max_relative_growth + - shift(Carrier_additions, along=period, offset=1, edge=0) * Carrier_relative_growth <= Carrier_max_growth ``` ```math -\mathit{Carrier\_additions}_{y,i} - \mathit{Carrier\_additions}_{y \boxminus_{0} 1,i} \cdot \mathrm{r}_{i} \le \overline{\Delta}_{i} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \overline{\Delta}_{i} \text{ is defined} +\mathit{Carrier\_additions}_{y,i} - \mathit{Carrier\_additions}_{y \boxminus_{0} 1,i} \cdot \mathrm{r}^{+}_{i} \le \overline{\Delta}_{i} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \overline{\Delta}_{i} \text{ is defined} ``` ### `CVaR-excess-{s}` @@ -8600,6 +8692,24 @@ Carrier_additions: \mathit{Carrier\_additions}_{y,i} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} + \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} + \sum_{j \in \mathcal{J} \,:\, \mathrm{Process\_carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} \qquad \forall\, y \in \mathcal{Y},\ i \in \mathcal{I} ``` +### `Carrier_relative_growth` + +```yaml +Carrier_relative_growth: + description: >- + the share of the previous period's additions a carrier's growth limit + reads — PyPSA's `max_relative_growth` clipped at zero, so a negative + share adds nothing and never tightens the limit + dims: [carrier] + cases: + positive: { when: Carrier_max_relative_growth > 0, expression: Carrier_max_relative_growth } + otherwise: 0 +``` + +```math +\mathrm{r}^{+}_{i} = \begin{cases} \mathrm{r}_{i} & \text{if } \mathrm{r}_{i} > 0 \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I} +``` + ### `Line_s_monitored` ```yaml diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 08010675..716bdb57 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -2024,6 +2024,15 @@ expressions: + sum(Store_e_nom_ext * Store_first_active, by=Store_carrier, over=store, into=carrier) + sum(Line_s_nom_ext * Line_first_active, by=Line_carrier, over=line, into=carrier) + sum(Process_p_nom_ext * Process_first_active, by=Process_carrier, over=process, into=carrier) + Carrier_relative_growth: + description: >- + the share of the previous period's additions a carrier's growth limit + reads — PyPSA's `max_relative_growth` clipped at zero, so a negative + share adds nothing and never tightens the limit + dims: [carrier] + cases: + positive: { when: Carrier_max_relative_growth > 0, expression: Carrier_max_relative_growth } + otherwise: 0 Line_s_monitored: description: >- the flow a line's post-contingency rows read — its flow where it stands, @@ -3572,7 +3581,7 @@ constraints: where: Carrier_max_growth expression: >- Carrier_additions - - shift(Carrier_additions, along=period, offset=1, edge=0) * Carrier_max_relative_growth + - shift(Carrier_additions, along=period, offset=1, edge=0) * Carrier_relative_growth <= Carrier_max_growth CVaR_excess: description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario" diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 9e2f49c4..2cf096b6 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1528,5 +1528,28 @@ "Process-fix-p-lower": 8, "Process-fix-p-upper": 8 } + }, + "rung_39_negative_relative_growth": { + "columns": { + "Generator-p": 8, + "Store-e": 6, + "Store-e_nom": 2, + "Store-p": 6 + }, + "global_constraints": {}, + "objective": 8600.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 4, + "Carrier-growth_limit": 2, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Store-energy_balance": 6, + "Store-ext-e-lower": 6, + "Store-ext-e-upper": 6, + "Store-ext-e_nom-lower": 2, + "Store-ext-e_nom-upper": 2 + } } } diff --git a/examples/references/pypsa/rung_39_negative_relative_growth.py b/examples/references/pypsa/rung_39_negative_relative_growth.py new file mode 100644 index 00000000..00ade931 --- /dev/null +++ b/examples/references/pypsa/rung_39_negative_relative_growth.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 39: a negative relative growth adds nothing to a carrier's growth limit — PyPSA clips it at zero.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: a battery carrier with `max_relative_growth=-0.5` builds in both periods.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', 'grid') + n.add('Carrier', 'solar') + n.add('Carrier', 'gas') + n.add('Carrier', 'battery', max_growth=20, max_relative_growth=-0.5) + n.add('Generator', 'solar', bus='grid', carrier='solar', p_nom=100, marginal_cost=1, p_max_pu=[1, 0, 1, 0]) + n.add('Generator', 'backup', bus='grid', carrier='gas', p_nom=200, marginal_cost=80) + n.add( + 'Store', + 'tank20', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=100, + capital_cost=10, + build_year=2020, + lifetime=30, + ) + n.add( + 'Store', + 'tank30', + bus='grid', + carrier='battery', + e_nom_extendable=True, + e_nom_max=100, + capital_cost=8, + build_year=2030, + lifetime=30, + ) + n.add('Load', 'town', bus='grid', p_set=[40, 60, 40, 80]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 6852461b..f6789c47 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -318,6 +318,7 @@ names: Process_first_active: '\mathrm{new}^{z}' Carrier_max_growth: '\overline{\Delta}' Carrier_max_relative_growth: '\mathrm{r}' + Carrier_relative_growth: '\mathrm{r}^{+}' Generator_maintainable: '\mathrm{mnt}' Link_maintainable: '\mathrm{mnt}^{f}' Process_maintainable: '\mathrm{mnt}^{z}' From 49a8fbd7d83456fe0dbfed724b15b94adfc86e87 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 14:47:59 +0200 Subject: [PATCH 27/46] docs(pypsa): a tech capacity expansion limit on a network with several scenarios is refused, as pypsa does --- docs/examples/pypsa.md | 19 +++++++++++++++++++ examples/pypsa.yaml | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 4c07528f..ab57959e 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -3352,6 +3352,7 @@ where it should live — language, data prep, or harness — is one open questio | `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` and `Transformer_loss_max` | X4 | | `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | | `ValueError`, `abstract.py:427`, `:445` | a security-constrained run over scenarios | rows per scenario, not refused | | +| `NotImplementedError`, `global_constraints.py:66-68` | a `tech_capacity_expansion_limit` row on a network with scenarios | assumed where there is more than one scenario: [`GlobalConstraint_tech_capacity_expansion_limit_without_scenarios`](#globalconstraint_tech_capacity_expansion_limit_without_scenarios). The file cannot tell one scenario from none, which PyPSA also refuses | | | `ConsistencyError`, `consistency.py:1506-1560` | a maintainable component whose `maintenance_duration` or `maintenance_events` is not positive, whose events do not fit the weighted horizon, or that is extendable with `p_nom_max = inf` | assumed: [`Generator_maintenance_events_positive`](#generator_maintenance_events_positive), [`-duration_positive`](#generator_maintenance_duration_positive), [`-duration_fits_the_horizon`](#generator_maintenance_duration_fits_the_horizon), [`-events_fit_the_horizon`](#generator_maintenance_events_fit_the_horizon), [`-build_cap_is_finite`](#generator_maintenance_build_cap_is_finite), and the `Link` and `Process` ones | | | nothing; HiGHS refuses the model, `constraints.py:500-503` | a fixed modular committable maintainable build, whose module count `p_nom_max / p_nom_mod` is infinite | assumed: [`Generator_maintenance_module_count_is_finite`](#generator_maintenance_module_count_is_finite), and the `Link` and `Process` ones | | @@ -9609,6 +9610,24 @@ Store_marginal_cost_quadratic_without_risk_preference: ```math \mathrm{c}^{q,(2)}_{t,v} = 0 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \omega > 0 ``` + +### `GlobalConstraint_tech_capacity_expansion_limit_without_scenarios` + +```yaml +GlobalConstraint_tech_capacity_expansion_limit_without_scenarios: + holds: "GlobalConstraint_type != 'tech_capacity_expansion_limit'" + where: "count(scenario_weight, over=scenario) > 1" + description: >- + PyPSA does not build a `tech_capacity_expansion_limit` row on a + network with scenarios and refuses it + (`global_constraints.py:66-68`). The spec cannot tell a network with + one scenario from one with none, so it refuses only where there is + more than one scenario +``` + +```math +\mathrm{type}_{i} \neq \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \qquad \forall\, i \in \mathcal{I} \,:\, \lvert \{ \xi \in \Xi \,:\, \pi_{\xi} \text{ is defined} \} \rvert > 1 +``` Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 716bdb57..3bdaeede 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -3840,6 +3840,15 @@ assumptions: refuses quadratic costs under any risk preference (`optimize.py:467-474`). The spec cannot tell no risk preference from one with `omega = 0`, so it refuses only where `omega` is positive + GlobalConstraint_tech_capacity_expansion_limit_without_scenarios: + holds: "GlobalConstraint_type != 'tech_capacity_expansion_limit'" + where: "count(scenario_weight, over=scenario) > 1" + description: >- + PyPSA does not build a `tech_capacity_expansion_limit` row on a + network with scenarios and refuses it + (`global_constraints.py:66-68`). The spec cannot tell a network with + one scenario from one with none, so it refuses only where there is + more than one scenario objective: sense: minimize From 0f578c7c141e24b8da175e019ea2e48e5508de9f Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 14:48:53 +0200 Subject: [PATCH 28/46] docs(pypsa): the carrier growth rung says a transformer counts in no carrier --- docs/examples/pypsa.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index ab57959e..9198b7a8 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1777,8 +1777,8 @@ extendable component of that carrier, not the generators alone. Here a battery carrier caps a storage unit and a store built in the first period, and the two builds fill the cap together. A store built in the later period adds its allowance plus half of what the carrier added before. PyPSA counts the -components that carry a carrier attribute; the spec extends the same limit to -transformers, which PyPSA leaves out. +components that carry a carrier attribute, and so does the spec, so a +transformer counts in no carrier. > ✔ `pypsa 1.3.0` solves this rung's network at objective `8452.5`, 74 rows. From 6bfa0529f95dfdad3bcff4cbd2117a662929ec53 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 16:05:44 +0200 Subject: [PATCH 29/46] docs(pypsa): a global constraint takes its own constant and sense in each scenario --- docs/examples/pypsa.md | 152 ++++++++++++++---- examples/pypsa.yaml | 30 ++-- examples/references/pypsa/references.json | 41 +++++ .../rung_40_scenario_global_constraints.py | 53 ++++++ tests/test_pypsa_collapses.py | 10 +- tests/test_pypsa_references.py | 6 +- 6 files changed, 244 insertions(+), 48 deletions(-) create mode 100644 examples/references/pypsa/rung_40_scenario_global_constraints.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 9198b7a8..cc57953b 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -414,9 +414,9 @@ each type is three blocks by sense. | PyPSA type | status | note | | ------------------------------------- | ----------- | ------------------------------------------------- | -| [`primary_energy`](#primary_energy) | split | a block per sense — sense as data is beyond #70; carrier weights are prep; one period in rung 35 | -| [`operational_limit`](#operational_limit) | split | a block per sense; one period in rung 35 | -| [`transmission_volume_expansion_limit`](#transmission_volume_expansion_limit) | split | a block per sense; membership from PyPSA's carrier string is prep | +| [`primary_energy`](#primary_energy) | split | a block per sense — sense as data is beyond #70; carrier weights are prep; one period in rung 35; per scenario in rung 40 | +| [`operational_limit`](#operational_limit) | split | a block per sense; one period in rung 35; per scenario in rung 40 | +| [`transmission_volume_expansion_limit`](#transmission_volume_expansion_limit) | split | a block per sense; membership from PyPSA's carrier string is prep; per scenario in rung 40 | | [`transmission_expansion_cost_limit`](#transmission_expansion_cost_limit) | split | a block per sense | | [`tech_capacity_expansion_limit`](#tech_capacity_expansion_limit) | split | a block per sense | | `Bus-nom_min/max_{carrier}` | out | deprecated in PyPSA | @@ -3332,6 +3332,100 @@ def build(): +### Rung 40 — a global constraint per scenario + +`n.set_scenarios(...)` with `GlobalConstraint` rows whose `constant` and +`sense` differ between the scenarios. PyPSA builds one `GlobalConstraint-{name}` +row per scenario for `primary_energy`, `operational_limit` and +`transmission_volume_expansion_limit`, and reads each scenario's own sense and +constant (`global_constraints.py:361-371`, `:556-557`, `:748-749`, +`:786-795`, `:860-861`). The file states `GlobalConstraint_constant` and +`GlobalConstraint_sense` over `scenario` as well, so each row takes its own +value in each future. A row with no scenario axis in its total, such as the +transmission volume, repeats the same capacity sum under each scenario's +constant. A plain run feeds one scenario, and the rows collapse to the standard +ones. + +The rung puts an extendable line under a volume limit of `60` in the calm +future and `20` in the stormy one, and a CO2 row that is at most `250` in the +calm future and exactly `200` in the stormy one. All three bind. With the same +network and the calm values in both futures, PyPSA solves to +`12943.333333333334`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`primary_energy`](#primary_energy), [`operational_limit`](#operational_limit), [`transmission_volume_expansion_limit`](#transmission_volume_expansion_limit) with a constant and sense per scenario | done | `GlobalConstraint_constant` and `GlobalConstraint_sense` over `scenario` | +| `transmission_expansion_cost_limit` on a network with scenarios | out | PyPSA `1.3.0` builds no row: it matches extendable names against a table indexed by scenario and name, and finds none (`global_constraints.py:916`). The file builds the row per scenario | +| `transmission_volume_expansion_limit` on a network with scenarios and `multi_investment_periods` | out | PyPSA `1.3.0` builds no row: the active-asset filter reindexes a table indexed by scenario and name by the names alone, and keeps none (`global_constraints.py:828`, `descriptors.py:261-263`). The file builds the row per scenario | +| a `carrier_attribute` or `investment_period` per scenario | out | PyPSA reads both per scenario (`global_constraints.py:797-802`); the file states both per row, in the weights and in `GlobalConstraint_counts_snapshot` | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `15106.666666666666`, 86 rows. + +
+The network, as PyPSA code + +`rung_40_scenario_global_constraints.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 40: a global constraint takes its own constant and sense in each scenario.""" + +from __future__ import annotations + +import spine + +#: each scenario's own constant and sense, per row +PER_SCENARIO = { + ('calm', 'volume40'): {'constant': 60}, + ('stormy', 'volume40'): {'constant': 20}, + ('calm', 'co2_40'): {'constant': 250, 'sense': '<='}, + ('stormy', 'co2_40'): {'constant': 200, 'sense': '=='}, +} + + +def build(): + """The spine over two futures, with an extendable line under a volume limit and a CO2 row that differ by scenario.""" + n = spine.build() + n.add('Carrier', 'AC') + n.add('Carrier', 'coalc', co2_emissions=0.5) + n.c.generators.static.loc['coal', 'carrier'] = 'coalc' + n.add( + 'Line', + 'tie40', + bus0='north', + bus1='south', + x=0.1, + carrier='AC', + length=2, + s_nom_extendable=True, + capital_cost=1, + ) + n.add('Load', 'port40', bus='south', p_set=30) + n.add( + 'GlobalConstraint', + 'volume40', + type='transmission_volume_expansion_limit', + carrier_attribute='AC', + sense='<=', + constant=60, + ) + n.add( + 'GlobalConstraint', 'co2_40', type='primary_energy', carrier_attribute='co2_emissions', sense='<=', constant=250 + ) + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + for row, values in PER_SCENARIO.items(): + for column, value in values.items(): + n.c.global_constraints.static.loc[row, column] = value + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3611,8 +3705,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{a}^{\sigma}`$ | `Transformer_loss_slope` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — the slope of a cut to a transformer's loss curve — a tangent's `2 * r_pu_eff * p_k`, a secant's `r_pu_eff * (p_k + p_k+1)`, as a line's, over the transformer's own `r_pu_eff` and rating, data prep | | $`\mathrm{b}^{\sigma}`$ | `Transformer_loss_offset` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | | $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{I}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | -| $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\mathcal{I}`$ — which way the row binds — `<=`, `>=` or `==` | -| $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, times its period's years for each counted period where the storage reopens per period, or a non-extendable build — is folded in here by data prep | +| $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\Xi \times \mathcal{I}`$ — which way the row binds in each scenario — `<=`, `>=` or `==`; PyPSA reads a row's sense per scenario (`global_constraints.py:556`, `:748`, `:860`) | +| $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\Xi \times \mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, times its period's years for each counted period where the storage reopens per period, or a non-extendable build — is folded in here by data prep. PyPSA reads it per scenario (`global_constraints.py:557`, `:749`, `:861`) | | $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\mathcal{I} \times \mathcal{T}`$ — whether a row counts a snapshot — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:377`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:375`) | | $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\mathcal{I} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency, data prep; a generator of an unweighted carrier has no row | | $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\mathcal{I} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | @@ -7474,7 +7568,7 @@ GlobalConstraint_primary_energy_ub: ``` ```math -\mathit{primary\_energy}_{\xi,i} \le \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} +\mathit{primary\_energy}_{\xi,i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} ``` ### `primary_energy` @@ -7490,7 +7584,7 @@ GlobalConstraint_primary_energy_lb: ``` ```math -\mathit{primary\_energy}_{\xi,i} \ge \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} +\mathit{primary\_energy}_{\xi,i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} ``` ### `primary_energy` @@ -7506,7 +7600,7 @@ GlobalConstraint_primary_energy_eq: ``` ```math -\mathit{primary\_energy}_{\xi,i} = \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} +\mathit{primary\_energy}_{\xi,i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{primary\_energy}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} ``` ### `operational_limit` @@ -7522,7 +7616,7 @@ GlobalConstraint_operational_limit_ub: ``` ```math -\mathit{operational\_limit}_{\xi,i} \le \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} +\mathit{operational\_limit}_{\xi,i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} ``` ### `operational_limit` @@ -7538,7 +7632,7 @@ GlobalConstraint_operational_limit_lb: ``` ```math -\mathit{operational\_limit}_{\xi,i} \ge \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} +\mathit{operational\_limit}_{\xi,i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} ``` ### `operational_limit` @@ -7554,7 +7648,7 @@ GlobalConstraint_operational_limit_eq: ``` ```math -\mathit{operational\_limit}_{\xi,i} = \mathrm{K}_{i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} +\mathit{operational\_limit}_{\xi,i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{operational\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -7564,13 +7658,13 @@ GlobalConstraint_operational_limit_eq: ```yaml GlobalConstraint_transmission_volume_expansion_limit_ub: description: "`transmission_volume_expansion_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_volume_expansion_limit' AND GlobalConstraint_sense == '<=' expression: transmission_volume_expansion <= GlobalConstraint_constant ``` ```math -\mathit{transmission\_volume\_expansion}_{i} \le \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} +\mathit{transmission\_volume\_expansion}_{i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -7580,13 +7674,13 @@ GlobalConstraint_transmission_volume_expansion_limit_ub: ```yaml GlobalConstraint_transmission_volume_expansion_limit_lb: description: "`transmission_volume_expansion_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_volume_expansion_limit' AND GlobalConstraint_sense == '>=' expression: transmission_volume_expansion >= GlobalConstraint_constant ``` ```math -\mathit{transmission\_volume\_expansion}_{i} \ge \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} +\mathit{transmission\_volume\_expansion}_{i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -7596,13 +7690,13 @@ GlobalConstraint_transmission_volume_expansion_limit_lb: ```yaml GlobalConstraint_transmission_volume_expansion_limit_eq: description: "`transmission_volume_expansion_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_volume_expansion_limit' AND GlobalConstraint_sense == '==' expression: transmission_volume_expansion == GlobalConstraint_constant ``` ```math -\mathit{transmission\_volume\_expansion}_{i} = \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} +\mathit{transmission\_volume\_expansion}_{i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -7612,13 +7706,13 @@ GlobalConstraint_transmission_volume_expansion_limit_eq: ```yaml GlobalConstraint_transmission_expansion_cost_limit_ub: description: "`transmission_expansion_cost_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_expansion_cost_limit' AND GlobalConstraint_sense == '<=' expression: transmission_expansion_cost <= GlobalConstraint_constant ``` ```math -\mathit{transmission\_expansion\_cost}_{i} \le \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} +\mathit{transmission\_expansion\_cost}_{i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -7628,13 +7722,13 @@ GlobalConstraint_transmission_expansion_cost_limit_ub: ```yaml GlobalConstraint_transmission_expansion_cost_limit_lb: description: "`transmission_expansion_cost_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_expansion_cost_limit' AND GlobalConstraint_sense == '>=' expression: transmission_expansion_cost >= GlobalConstraint_constant ``` ```math -\mathit{transmission\_expansion\_cost}_{i} \ge \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} +\mathit{transmission\_expansion\_cost}_{i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -7644,13 +7738,13 @@ GlobalConstraint_transmission_expansion_cost_limit_lb: ```yaml GlobalConstraint_transmission_expansion_cost_limit_eq: description: "`transmission_expansion_cost_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_expansion_cost_limit' AND GlobalConstraint_sense == '==' expression: transmission_expansion_cost == GlobalConstraint_constant ``` ```math -\mathit{transmission\_expansion\_cost}_{i} = \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} +\mathit{transmission\_expansion\_cost}_{i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} ``` ### `tech_capacity_expansion_limit` @@ -7660,13 +7754,13 @@ GlobalConstraint_transmission_expansion_cost_limit_eq: ```yaml GlobalConstraint_tech_capacity_expansion_limit_ub: description: "`tech_capacity_expansion_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'tech_capacity_expansion_limit' AND GlobalConstraint_sense == '<=' expression: tech_capacity_expansion <= GlobalConstraint_constant ``` ```math -\mathit{tech\_capacity\_expansion}_{i} \le \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{<=}\text{'} +\mathit{tech\_capacity\_expansion}_{i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} ``` ### `tech_capacity_expansion_limit` @@ -7676,13 +7770,13 @@ GlobalConstraint_tech_capacity_expansion_limit_ub: ```yaml GlobalConstraint_tech_capacity_expansion_limit_lb: description: "`tech_capacity_expansion_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'tech_capacity_expansion_limit' AND GlobalConstraint_sense == '>=' expression: tech_capacity_expansion >= GlobalConstraint_constant ``` ```math -\mathit{tech\_capacity\_expansion}_{i} \ge \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{>=}\text{'} +\mathit{tech\_capacity\_expansion}_{i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} ``` ### `tech_capacity_expansion_limit` @@ -7692,13 +7786,13 @@ GlobalConstraint_tech_capacity_expansion_limit_lb: ```yaml GlobalConstraint_tech_capacity_expansion_limit_eq: description: "`tech_capacity_expansion_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'tech_capacity_expansion_limit' AND GlobalConstraint_sense == '==' expression: tech_capacity_expansion == GlobalConstraint_constant ``` ```math -\mathit{tech\_capacity\_expansion}_{i} = \mathrm{K}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{i} = \text{'}\mathrm{==}\text{'} +\mathit{tech\_capacity\_expansion}_{i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} ``` ### `Bus-nodal_balance` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 3bdaeede..a58374b9 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -948,16 +948,20 @@ parameters: dims: [global_constraint] dtype: str GlobalConstraint_sense: - description: which way the row binds — `<=`, `>=` or `==` - dims: [global_constraint] + description: >- + which way the row binds in each scenario — `<=`, `>=` or `==`; PyPSA + reads a row's sense per scenario (`global_constraints.py:556`, `:748`, + `:860`) + dims: [scenario, global_constraint] dtype: str GlobalConstraint_constant: description: >- the constant the total is held against; what a variable cannot carry — an initial charge, times its period's years for each counted period where the storage reopens per period, or a non-extendable build — is - folded in here by data prep - dims: [global_constraint] + folded in here by data prep. PyPSA reads it per scenario + (`global_constraints.py:557`, `:749`, `:861`) + dims: [scenario, global_constraint] GlobalConstraint_counts_snapshot: description: >- whether a row counts a snapshot — PyPSA's `investment_period`: every @@ -3500,47 +3504,47 @@ constraints: expression: operational_limit == GlobalConstraint_constant GlobalConstraint_transmission_volume_expansion_limit_ub: description: "`transmission_volume_expansion_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_volume_expansion_limit' AND GlobalConstraint_sense == '<=' expression: transmission_volume_expansion <= GlobalConstraint_constant GlobalConstraint_transmission_volume_expansion_limit_lb: description: "`transmission_volume_expansion_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_volume_expansion_limit' AND GlobalConstraint_sense == '>=' expression: transmission_volume_expansion >= GlobalConstraint_constant GlobalConstraint_transmission_volume_expansion_limit_eq: description: "`transmission_volume_expansion_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_volume_expansion_limit' AND GlobalConstraint_sense == '==' expression: transmission_volume_expansion == GlobalConstraint_constant GlobalConstraint_transmission_expansion_cost_limit_ub: description: "`transmission_expansion_cost_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_expansion_cost_limit' AND GlobalConstraint_sense == '<=' expression: transmission_expansion_cost <= GlobalConstraint_constant GlobalConstraint_transmission_expansion_cost_limit_lb: description: "`transmission_expansion_cost_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_expansion_cost_limit' AND GlobalConstraint_sense == '>=' expression: transmission_expansion_cost >= GlobalConstraint_constant GlobalConstraint_transmission_expansion_cost_limit_eq: description: "`transmission_expansion_cost_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'transmission_expansion_cost_limit' AND GlobalConstraint_sense == '==' expression: transmission_expansion_cost == GlobalConstraint_constant GlobalConstraint_tech_capacity_expansion_limit_ub: description: "`tech_capacity_expansion_limit` — its total, at most its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'tech_capacity_expansion_limit' AND GlobalConstraint_sense == '<=' expression: tech_capacity_expansion <= GlobalConstraint_constant GlobalConstraint_tech_capacity_expansion_limit_lb: description: "`tech_capacity_expansion_limit` — its total, at least its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'tech_capacity_expansion_limit' AND GlobalConstraint_sense == '>=' expression: tech_capacity_expansion >= GlobalConstraint_constant GlobalConstraint_tech_capacity_expansion_limit_eq: description: "`tech_capacity_expansion_limit` — its total, at its constant" - dims: [global_constraint] + dims: [scenario, global_constraint] where: GlobalConstraint_type == 'tech_capacity_expansion_limit' AND GlobalConstraint_sense == '==' expression: tech_capacity_expansion == GlobalConstraint_constant Bus_nodal_balance: diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 2cf096b6..a68d6c36 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1551,5 +1551,46 @@ "Store-ext-e_nom-lower": 2, "Store-ext-e_nom-upper": 2 } + }, + "rung_40_scenario_global_constraints": { + "columns": { + "Generator-p": 16, + "Line-s": 8, + "Line-s_nom": 1, + "Link-p": 8 + }, + "global_constraints": { + "('calm', 'co2_40')": { + "sense": "<=", + "type": "primary_energy" + }, + "('calm', 'volume40')": { + "sense": "<=", + "type": "transmission_volume_expansion_limit" + }, + "('stormy', 'co2_40')": { + "sense": "==", + "type": "primary_energy" + }, + "('stormy', 'volume40')": { + "sense": "<=", + "type": "transmission_volume_expansion_limit" + } + }, + "objective": 15106.666666666666, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 16, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "GlobalConstraint-co2_40": 2, + "GlobalConstraint-volume40": 2, + "Line-ext-s-lower": 8, + "Line-ext-s-upper": 8, + "Line-ext-s_nom-lower": 2, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8 + } } } diff --git a/examples/references/pypsa/rung_40_scenario_global_constraints.py b/examples/references/pypsa/rung_40_scenario_global_constraints.py new file mode 100644 index 00000000..5dd1679e --- /dev/null +++ b/examples/references/pypsa/rung_40_scenario_global_constraints.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 40: a global constraint takes its own constant and sense in each scenario.""" + +from __future__ import annotations + +import spine + +#: each scenario's own constant and sense, per row +PER_SCENARIO = { + ('calm', 'volume40'): {'constant': 60}, + ('stormy', 'volume40'): {'constant': 20}, + ('calm', 'co2_40'): {'constant': 250, 'sense': '<='}, + ('stormy', 'co2_40'): {'constant': 200, 'sense': '=='}, +} + + +def build(): + """The spine over two futures, with an extendable line under a volume limit and a CO2 row that differ by scenario.""" + n = spine.build() + n.add('Carrier', 'AC') + n.add('Carrier', 'coalc', co2_emissions=0.5) + n.c.generators.static.loc['coal', 'carrier'] = 'coalc' + n.add( + 'Line', + 'tie40', + bus0='north', + bus1='south', + x=0.1, + carrier='AC', + length=2, + s_nom_extendable=True, + capital_cost=1, + ) + n.add('Load', 'port40', bus='south', p_set=30) + n.add( + 'GlobalConstraint', + 'volume40', + type='transmission_volume_expansion_limit', + carrier_attribute='AC', + sense='<=', + constant=60, + ) + n.add( + 'GlobalConstraint', 'co2_40', type='primary_energy', carrier_attribute='co2_emissions', sense='<=', constant=250 + ) + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + for row, values in PER_SCENARIO.items(): + for column, value in values.items(): + n.c.global_constraints.static.loc[row, column] = value + return n diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index 344a8561..c91d5b3d 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -75,13 +75,17 @@ def test_a_standard_constraint_changes_frame_only_by_a_leading_scenario(): assert not wrong, f'a standard row may gain only a leading scenario; these differ: {wrong}' -def test_only_load_carries_scenario_among_standard_parameters(): +#: the standard parameters PyPSA reads per scenario +SCENARIO_PARAMETERS = {'Load_p_set', 'GlobalConstraint_sense', 'GlobalConstraint_constant'} + + +def test_only_the_scenario_parameters_carry_scenario_among_standard_parameters(): wrong = { name: _dims(ALL.parameters[name]) for name, dims in STANDARD['parameters'].items() - if _dims(ALL.parameters[name]) != (['scenario', *dims] if name == 'Load_p_set' else dims) + if _dims(ALL.parameters[name]) != (['scenario', *dims] if name in SCENARIO_PARAMETERS else dims) } - assert not wrong, f'only Load_p_set gains scenario; these differ: {wrong}' + assert not wrong, f'only {sorted(SCENARIO_PARAMETERS)} gain scenario; these differ: {wrong}' def test_the_extra_axes_and_rows_are_declared(): diff --git a/tests/test_pypsa_references.py b/tests/test_pypsa_references.py index aae55ab7..d33c9cf7 100644 --- a/tests/test_pypsa_references.py +++ b/tests/test_pypsa_references.py @@ -148,9 +148,9 @@ def test_the_spine_weightings_are_generic(): assert 1.0 not in values, f'{name} carries a 1.0 — the identity a missing factor hides behind' -@pytest.mark.parametrize('row', sorted(row for row in RECORDED_ROWS if row.startswith('GlobalConstraint-')), ids=str) -def test_a_global_constraint_row_has_a_block_of_its_recorded_type_and_sense(row: str): - gc = GC_RECORDED[row.removeprefix('GlobalConstraint-')] +@pytest.mark.parametrize('label', sorted(GC_RECORDED), ids=str) +def test_a_global_constraint_row_has_a_block_of_its_recorded_type_and_sense(label: str): + gc = GC_RECORDED[label] matching = [ name for m in MODELS From 0db274a24a104652ef6498bffce8897299d4c557 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 24 Sep 2026 16:42:46 +0200 Subject: [PATCH 30/46] docs(pypsa): the stochastic rung says which component data spans a scenario --- docs/examples/pypsa.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index cc57953b..5bc57de5 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1299,6 +1299,14 @@ excess per scenario and the tail's average, blended into the objective at (`optimize.py:458`). The file builds them only where `omega` is positive, so a plain run, and a risk preference with `omega = 0`, has none. +Component data does not span a scenario yet. PyPSA reads almost every +attribute per scenario, and refuses a difference only in the ones that fix +the network's shape, such as `bus`, `carrier`, `lifetime` or +`p_nom_extendable` (`consistency.py:1174-1195`). The file gives a scenario +axis only to a load's `p_set` and a global constraint's `sense` and +`constant`. This rung's wind `p_max_pu` differs by scenario, so the file +states the rung's rows but not all of its data. + | PyPSA | status | note | | --- | --- | --- | | [`Generator-p`, `Link-p`](#variable-domains) | done | over `scenario`; `Generator-p_nom` is not — chosen once | @@ -1307,6 +1315,7 @@ plain run, and a risk preference with `omega = 0`, has none. | [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension; none where `omega` is zero | | [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep; none where `omega` is zero | | [objective](#objective) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | +| `Generator-p_max_pu` per scenario | out | the file holds one availability for every scenario; only a load's `p_set` spans one | > ✔ `pypsa 1.3.0` solves this rung's network at objective `9267.386666666665`, 87 rows. From e52b90d26e3a9acd8246633cd461fa8c793814db Mon Sep 17 00:00:00 2001 From: Fabian Hofmann Date: Thu, 24 Sep 2026 17:09:22 +0200 Subject: [PATCH 31/46] 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/examples/pypsa.md | 1191 +++++++++-------- examples/pypsa.yaml | 556 ++++---- examples/references/pypsa/references.json | 39 + .../rung_41_scenario_operational_data.py | 31 + .../pypsa/rung_42_scenario_first_stage.py | 26 + tests/test_pypsa_collapses.py | 23 +- 6 files changed, 1073 insertions(+), 793 deletions(-) create mode 100644 examples/references/pypsa/rung_41_scenario_operational_data.py create mode 100644 examples/references/pypsa/rung_42_scenario_first_stage.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 5bc57de5..ea99fdb6 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1299,13 +1299,22 @@ excess per scenario and the tail's average, blended into the objective at (`optimize.py:458`). The file builds them only where `omega` is positive, so a plain run, and a risk preference with `omega = 0`, has none. -Component data does not span a scenario yet. PyPSA reads almost every -attribute per scenario, and refuses a difference only in the ones that fix -the network's shape, such as `bus`, `carrier`, `lifetime` or -`p_nom_extendable` (`consistency.py:1174-1195`). The file gives a scenario -axis only to a load's `p_set` and a global constraint's `sense` and -`constant`. This rung's wind `p_max_pu` differs by scenario, so the file -states the rung's rows but not all of its data. +A parameter spans `scenario` exactly when PyPSA reads it per scenario. PyPSA +reads component data through `c.da`, one value per scenario +(`components/array.py:332-395`), so almost every parameter spans one. It +refuses a difference in the attributes that fix the network's shape, such as +`bus`, `carrier`, `lifetime`, `active`, `committable` or `p_nom_extendable` +(`consistency.py:1174-1195`), and these parameters and what data prep derives +from them span none. Some other parameters span none either. PyPSA reduces +`maintainable` to a union over the scenarios, `(p_min_pu >= 0).all()` over +them, and a carrier's growth limits to their least value +(`components.py:1016-1019`, `constraints.py:397-401`, +`global_constraints.py:226-230`). It builds the cycle basis from the first +scenario (`networks.py:1354-1361`). A link's delay and a transformer's phase +shift span none, because PyPSA `1.3.0` mishandles them over scenarios (rung +41). A branch's `BODF` spans none, because PyPSA refuses a +security-constrained run over scenarios. This rung's wind `p_max_pu` differs by scenario, and the file states it over +`scenario`. Rungs 41 and 42 make operating data and first-stage data differ. | PyPSA | status | note | | --- | --- | --- | @@ -1314,8 +1323,8 @@ states the rung's rows but not all of its data. | [`CVaR-a`, `CVaR-theta`, `CVaR`](#variable-domains) | done | | | [`CVaR-excess-{s}`](#cvar-excess-s) | split | PyPSA names a row per scenario; one block over the dimension; none where `omega` is zero | | [`CVaR-def`](#cvar-def) | done | `1 / (1 - alpha)` is data prep; none where `omega` is zero | -| [objective](#objective) | done | capacity once; operation `(1 - omega)` in expectation, `omega` at the tail | -| `Generator-p_max_pu` per scenario | out | the file holds one availability for every scenario; only a load's `p_set` spans one | +| [objective](#objective) | done | capacity once, at its capital cost in expectation over the scenarios; operation `(1 - omega)` in expectation, `omega` at the tail | +| `Generator-p_max_pu` and other component data per scenario | done | every parameter PyPSA reads per scenario spans `scenario`; operating data in rung 41, first-stage bounds and capital cost in rung 42 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `9267.386666666665`, 87 rows. @@ -3366,7 +3375,7 @@ network and the calm values in both futures, PyPSA solves to | [`primary_energy`](#primary_energy), [`operational_limit`](#operational_limit), [`transmission_volume_expansion_limit`](#transmission_volume_expansion_limit) with a constant and sense per scenario | done | `GlobalConstraint_constant` and `GlobalConstraint_sense` over `scenario` | | `transmission_expansion_cost_limit` on a network with scenarios | out | PyPSA `1.3.0` builds no row: it matches extendable names against a table indexed by scenario and name, and finds none (`global_constraints.py:916`). The file builds the row per scenario | | `transmission_volume_expansion_limit` on a network with scenarios and `multi_investment_periods` | out | PyPSA `1.3.0` builds no row: the active-asset filter reindexes a table indexed by scenario and name by the names alone, and keeps none (`global_constraints.py:828`, `descriptors.py:261-263`). The file builds the row per scenario | -| a `carrier_attribute` or `investment_period` per scenario | out | PyPSA reads both per scenario (`global_constraints.py:797-802`); the file states both per row, in the weights and in `GlobalConstraint_counts_snapshot` | +| a `carrier_attribute` or `investment_period` per scenario | done | PyPSA reads both per scenario (`global_constraints.py:797-802`); the weights and `GlobalConstraint_counts_snapshot` span `scenario` | > ✔ `pypsa 1.3.0` solves this rung's network at objective `15106.666666666666`, 86 rows. @@ -3435,6 +3444,140 @@ def build(): +### Rung 41 — operating data per scenario + +`n.set_scenarios(...)` with a gas unit's `marginal_cost` and a link's +`efficiency` set per scenario. PyPSA reads both through `c.da`, one value per +scenario, into the objective and into `Bus-nodal_balance` +(`components/array.py:332-395`). The file states `Generator_marginal_cost` and +`Link_efficiency` over `scenario`, as it states every parameter PyPSA reads per +scenario. A plain run feeds one scenario, and the rows collapse to the standard +ones. + +The rung adds a gas unit that costs `20` in the calm future and `80` in the +stormy one, and a second link that delivers `0.9` and `0.6` of its flow. Each +binds. With the calm cost in both futures, PyPSA solves to `16308.0`; with the +calm efficiency in both, to `16992.0`; with both calm values in both, to +`15660.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [objective](#objective), [`Bus-nodal_balance`](#bus-nodal_balance) with a cost and an efficiency per scenario | done | `Generator_marginal_cost` and `Link_efficiency` over `scenario` | +| a link `delay` or `cyclic_delay` that differs by scenario | out | PyPSA `1.3.0` groups the ports by delay over all scenarios and shifts each group in every scenario, so a delay of `0` in one future and `1` in the other solves below both uniform networks (`constraints.py:1269`). The file holds one delay for every scenario | +| a transformer in a cycle on a network with scenarios | out | PyPSA `1.3.0` fails: it selects the transformers of a cycle by name from a table indexed by scenario and name (`constraints.py:1654`). The file holds one phase shift for every scenario | +| a committable component on a network with scenarios | out | PyPSA `1.3.0` fails: it selects the status by snapshot and name where the first dimension is the scenario (`constraints.py:1872`, `:1942`). The file builds the rows per scenario | +| [`{c}-p_nom_set`](#generator-p_nom_set) on a network with scenarios | out | PyPSA `1.3.0` fails: it reindexes the build by a table indexed by scenario and name (`constraints.py:1708`). The file builds the row per scenario | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `17964.0`, 96 rows. + +
+The network, as PyPSA code + +`rung_41_scenario_operational_data.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 41: a unit's cost and a link's efficiency differ by scenario.""" + +from __future__ import annotations + +import spine + +#: each scenario's own value, per component and attribute +PER_SCENARIO = { + ('calm', 'gas41'): {'marginal_cost': 20}, + ('stormy', 'gas41'): {'marginal_cost': 80}, + ('calm', 'wire41'): {'efficiency': 0.9}, + ('stormy', 'wire41'): {'efficiency': 0.6}, +} + + +def build(): + """The spine over two futures, with a gas unit that costs more and a link that delivers less in the stormy one.""" + n = spine.build() + n.add('Generator', 'gas41', bus='south', p_nom=100, marginal_cost=20) + n.add('Link', 'wire41', bus0='north', bus1='south', p_nom=40, efficiency=0.9) + n.add('Load', 'port41', bus='south', p_set=60) + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + for (scenario, name), values in PER_SCENARIO.items(): + component = n.c.generators if name == 'gas41' else n.c.links + for column, value in values.items(): + component.static.loc[(scenario, name), column] = value + return n +``` + +
+ + +### Rung 42 — first-stage data per scenario + +`n.set_scenarios(...)` with an extendable unit whose `capital_cost` and +`p_nom_max` differ between the scenarios. The build is chosen once, but PyPSA +reads its bounds per scenario and writes `Generator-ext-p_nom-lower` and +`-upper` once per scenario, so the tightest cap binds +(`constraints.py:885-895`). It prices the build at each scenario's capital +cost and weights the terms by the scenario weights (`optimize.py:405-412`, +`:448-454`), so the build pays its capital cost in expectation. The file states +`Generator_p_nom_max` and `Generator_capital_cost` over `scenario`, the bound +rows over `scenario`, and the capital terms of the objective under +`scenario_weight`. A plain run feeds one scenario of weight one, and the rows +and the objective collapse to the standard ones. + +The rung's wind unit costs `20` in the calm future and `60` in the stormy one, +weighted `0.6` and `0.4`, and may be built to `100` and `30`. PyPSA builds `30` +at the expected cost of `36`: the same network with a cost of `36` and a cap of +`30` in both futures solves to the same objective. With the calm values in both +futures, PyPSA solves to `1943.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`{c}-ext-p_nom-lower/upper`](#generator-ext-p_nom-lower) with a bound per scenario | done | one row per scenario, over `scenario`; the tightest binds | +| [capital cost](#objective) per scenario | done | `scenario_weight` times each scenario's capital cost | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `5049.999999999999`, 84 rows. + +
+The network, as PyPSA code + +`rung_42_scenario_first_stage.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 42: an extendable unit's capital cost and build cap differ by scenario.""" + +from __future__ import annotations + +import spine + +#: each scenario's own value, per attribute of the extendable unit +PER_SCENARIO = { + 'calm': {'capital_cost': 20, 'p_nom_max': 100}, + 'stormy': {'capital_cost': 60, 'p_nom_max': 30}, +} + + +def build(): + """The spine over two futures, with an extendable wind unit that costs more and may be built less in the stormy one.""" + n = spine.build() + n.add('Generator', 'wind42', bus='south', p_nom_extendable=True, p_nom_max=100, marginal_cost=1, capital_cost=20) + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + for scenario, values in PER_SCENARIO.items(): + for column, value in values.items(): + n.c.generators.static.loc[(scenario, 'wind42'), column] = value + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3466,19 +3609,19 @@ concatenation of the regime blocks, `p0`/`p1` derived from `Link-p`. ## The file -A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. Capacity is chosen once, before the future is known, and paid once per active period; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows, which stand only where that share is positive. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. +A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year and lifetime span. A parameter spans `scenario` exactly when PyPSA reads it per scenario. Capacity is chosen once, before the future is known, and paid once per active period at its cost in expectation over the scenarios; operation is the expectation over the scenarios' weights, with a share priced at the tail through the CVaR rows, which stand only where that share is positive. A plain run feeds one scenario, one period, all-active masks and unit weights, and the model collapses to the standard one. A security-constrained run copies each branch flow limit once per outage in an `outage` set that a plain run leaves empty. Which snapshots an asset is active in, a scenario's weight, and the outage factors are data prep. #### Sets | Symbol | Meaning | |---|---| -| $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y},\ \mathrm{Generator\_maintenance\_cover} \subseteq \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Link\_maintenance\_cover} \subseteq \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Process\_maintenance\_cover} \subseteq \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$ — dispatch periods | +| $`\Xi`$ | index $`\xi`$ — `scenario` with $`\mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$ — the futures dispatch is chosen in, each with a weight | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y},\ \mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$ — dispatch periods | | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Generator\_maintenance\_cover} \subseteq \mathcal{G} \times \mathcal{T} \times \mathcal{T}`$ — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_maintenance\_cover} \subseteq \mathcal{L} \times \mathcal{T} \times \mathcal{T}`$ — controllable connections, each from one bus to the buses it delivers to | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T}`$ — generating units, each on one bus | +| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T}`$ — controllable connections, each from one bus to the buses it delivers to | | $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_maintenance\_cover} \subseteq \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | +| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | | $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | | $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | @@ -3497,101 +3640,101 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | Symbol | Meaning | |---|---| | $`\mathrm{w}`$ | `snapshot_weightings_objective` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost | -| $`\mathrm{p}^{\mathrm{nom}}`$ | `Generator_p_nom` over $`\mathcal{G}`$ — nominal power | +| $`\mathrm{p}^{\mathrm{nom}}`$ | `Generator_p_nom` over $`\Xi \times \mathcal{G}`$ — nominal power | | $`\mathrm{ext}`$ | `Generator_p_nom_extendable` over $`\mathcal{G}`$ — whether the nominal power is a decision | -| $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | -| $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | -| $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | -| $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | +| $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | +| $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | +| $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | +| $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | | $`\mathrm{com}`$ | `Generator_committable` over $`\mathcal{G}`$ — whether output is gated by an on/off status decision | -| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\mathcal{G}`$ — most output in the snapshot a unit starts, per unit of nominal power | -| $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\mathcal{G}`$ — most output in the snapshot before a unit stops, per unit of nominal power | -| $`\mathrm{UT}`$ | `Generator_min_up_time` over $`\mathcal{G}`$ — least snapshots a unit stays on once started | -| $`\mathrm{DT}`$ | `Generator_min_down_time` over $`\mathcal{G}`$ — least snapshots a unit stays off once stopped | -| $`\mathrm{u}^{0}`$ | `Generator_status_initial` over $`\mathcal{G}`$ — one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | -| $`\mathrm{hold}`$ | `Generator_must_stay_up` over $`\mathcal{T} \times \mathcal{G}`$ — true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | -| $`\mathrm{rest}`$ | `Generator_must_stay_down` over $`\mathcal{T} \times \mathcal{G}`$ — true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | -| $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\mathcal{G}`$ — cost of one start | -| $`\mathrm{c}^{\mathrm{dn}}`$ | `Generator_shut_down_cost` over $`\mathcal{G}`$ — cost of one stop | -| $`\mathrm{c}^{\mathrm{on}}`$ | `Generator_stand_by_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one snapshot spent on | +| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot a unit starts, per unit of nominal power | +| $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot before a unit stops, per unit of nominal power | +| $`\mathrm{UT}`$ | `Generator_min_up_time` over $`\Xi \times \mathcal{G}`$ — least snapshots a unit stays on once started | +| $`\mathrm{DT}`$ | `Generator_min_down_time` over $`\Xi \times \mathcal{G}`$ — least snapshots a unit stays off once stopped | +| $`\mathrm{u}^{0}`$ | `Generator_status_initial` over $`\Xi \times \mathcal{G}`$ — one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{hold}`$ | `Generator_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | +| $`\mathrm{rest}`$ | `Generator_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | +| $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\Xi \times \mathcal{G}`$ — cost of one start | +| $`\mathrm{c}^{\mathrm{dn}}`$ | `Generator_shut_down_cost` over $`\Xi \times \mathcal{G}`$ — cost of one stop | +| $`\mathrm{c}^{\mathrm{on}}`$ | `Generator_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one snapshot spent on | | $`\mathrm{p}^{\mathrm{mod}}`$ | `Generator_p_nom_mod` over $`\mathcal{G}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | -| $`\mathrm{N}^{\mathrm{fix}}`$ | `Generator_modules_installed` over $`\mathcal{G}`$ — how many whole modules a committable build has in place: `Generator_p_nom / Generator_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | -| $`\mathrm{M}`$ | `Generator_big_m` over $`\mathcal{G}`$ — a bound safely above any feasible output — the build cap at full availability, data prep | -| $`\mathrm{nonneg}`$ | `Generator_p_min_pu_nonneg` over $`\mathcal{G}`$ — true where none of the generator's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | -| $`\mathrm{mnt}`$ | `Generator_maintainable` over $`\mathcal{G}`$ — whether a generator must be taken off for maintenance within the horizon | -| $`\gamma`$ | `Generator_maintenance_pu` over $`\mathcal{G}`$ — the share of the build a maintenance event takes off | -| $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\mathcal{G}`$ — how many maintenance events the horizon holds | -| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\mathcal{G}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | -| $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | -| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\mathcal{L}`$ — nominal power | +| $`\mathrm{N}^{\mathrm{fix}}`$ | `Generator_modules_installed` over $`\Xi \times \mathcal{G}`$ — how many whole modules a committable build has in place: `Generator_p_nom / Generator_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | +| $`\mathrm{M}`$ | `Generator_big_m` over $`\Xi \times \mathcal{G}`$ — a bound safely above any feasible output — the build cap at full availability, data prep | +| $`\mathrm{nonneg}`$ | `Generator_p_min_pu_nonneg` over $`\mathcal{G}`$ — true where none of the generator's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | +| $`\mathrm{mnt}`$ | `Generator_maintainable` over $`\mathcal{G}`$ — whether a generator must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | +| $`\gamma`$ | `Generator_maintenance_pu` over $`\Xi \times \mathcal{G}`$ — the share of the build a maintenance event takes off | +| $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\Xi \times \mathcal{G}`$ — how many maintenance events the horizon holds | +| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\Xi \times \mathcal{G}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | +| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\Xi \times \mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\Xi \times \mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\Xi \times \mathcal{L}`$ — nominal power | | $`\mathrm{ext}^{f}`$ | `Link_p_nom_extendable` over $`\mathcal{L}`$ — whether the nominal power is a decision | -| $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | -| $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | -| $`\eta`$ | `Link_efficiency` over $`\mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | -| $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once | -| $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at each period's first snapshots is lost | -| $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | -| $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | +| $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | +| $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | +| $`\eta`$ | `Link_efficiency` over $`\Xi \times \mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | +| $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once. One for every scenario: PyPSA groups the ports by delay over all scenarios and shifts each group in every one (`constraints.py:1269`) | +| $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at each period's first snapshots is lost. One for every scenario, as the delay | +| $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | +| $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | | $`\mathrm{com}^{f}`$ | `Link_committable` over $`\mathcal{L}`$ — whether flow is gated by an on/off status decision | -| $`\mathrm{ru}^{f,\mathrm{up}}`$ | `Link_ramp_limit_start_up` over $`\mathcal{L}`$ — most flow in the snapshot a link starts, per unit of nominal power | -| $`\mathrm{rd}^{f,\mathrm{dn}}`$ | `Link_ramp_limit_shut_down` over $`\mathcal{L}`$ — most flow in the snapshot before a link stops, per unit of nominal power | -| $`\mathrm{UT}^{f}`$ | `Link_min_up_time` over $`\mathcal{L}`$ — least snapshots a link stays on once started | -| $`\mathrm{DT}^{f}`$ | `Link_min_down_time` over $`\mathcal{L}`$ — least snapshots a link stays off once stopped | -| $`\mathrm{u}^{f,0}`$ | `Link_status_initial` over $`\mathcal{L}`$ — one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | -| $`\mathrm{hold}^{f}`$ | `Link_must_stay_up` over $`\mathcal{T} \times \mathcal{L}`$ — true while the up time a link brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | -| $`\mathrm{rest}^{f}`$ | `Link_must_stay_down` over $`\mathcal{T} \times \mathcal{L}`$ — true while the down time a link brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | -| $`\mathrm{c}^{f,\mathrm{up}}`$ | `Link_start_up_cost` over $`\mathcal{L}`$ — cost of one start | -| $`\mathrm{c}^{f,\mathrm{dn}}`$ | `Link_shut_down_cost` over $`\mathcal{L}`$ — cost of one stop | -| $`\mathrm{c}^{f,\mathrm{on}}`$ | `Link_stand_by_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one snapshot spent on | +| $`\mathrm{ru}^{f,\mathrm{up}}`$ | `Link_ramp_limit_start_up` over $`\Xi \times \mathcal{L}`$ — most flow in the snapshot a link starts, per unit of nominal power | +| $`\mathrm{rd}^{f,\mathrm{dn}}`$ | `Link_ramp_limit_shut_down` over $`\Xi \times \mathcal{L}`$ — most flow in the snapshot before a link stops, per unit of nominal power | +| $`\mathrm{UT}^{f}`$ | `Link_min_up_time` over $`\Xi \times \mathcal{L}`$ — least snapshots a link stays on once started | +| $`\mathrm{DT}^{f}`$ | `Link_min_down_time` over $`\Xi \times \mathcal{L}`$ — least snapshots a link stays off once stopped | +| $`\mathrm{u}^{f,0}`$ | `Link_status_initial` over $`\Xi \times \mathcal{L}`$ — one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{hold}^{f}`$ | `Link_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true while the up time a link brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | +| $`\mathrm{rest}^{f}`$ | `Link_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true while the down time a link brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | +| $`\mathrm{c}^{f,\mathrm{up}}`$ | `Link_start_up_cost` over $`\Xi \times \mathcal{L}`$ — cost of one start | +| $`\mathrm{c}^{f,\mathrm{dn}}`$ | `Link_shut_down_cost` over $`\Xi \times \mathcal{L}`$ — cost of one stop | +| $`\mathrm{c}^{f,\mathrm{on}}`$ | `Link_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one snapshot spent on | | $`\mathrm{f}^{\mathrm{mod}}`$ | `Link_p_nom_mod` over $`\mathcal{L}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | -| $`\mathrm{N}^{f,\mathrm{fix}}`$ | `Link_modules_installed` over $`\mathcal{L}`$ — how many whole modules a committable build has in place: `Link_p_nom / Link_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | -| $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\mathcal{L}`$ — a bound safely above any feasible flow — the build cap at full availability, data prep | -| $`\mathrm{nonneg}^{f}`$ | `Link_p_min_pu_nonneg` over $`\mathcal{L}`$ — true where none of the link's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | -| $`\mathrm{mnt}^{f}`$ | `Link_maintainable` over $`\mathcal{L}`$ — whether a link must be taken off for maintenance within the horizon | -| $`\gamma^{f}`$ | `Link_maintenance_pu` over $`\mathcal{L}`$ — the share of the build a maintenance event takes off | -| $`\mathrm{n}^{f,\mathrm{mnt}}`$ | `Link_maintenance_events` over $`\mathcal{L}`$ — how many maintenance events the horizon holds | -| $`\tau^{f,\mathrm{mnt}}`$ | `Link_maintenance_duration` over $`\mathcal{L}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the link is not maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon | -| $`\mathrm{blk}^{f}`$ | `Link_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{L}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the link does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | -| $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\mathcal{J}`$ — nominal internal power | +| $`\mathrm{N}^{f,\mathrm{fix}}`$ | `Link_modules_installed` over $`\Xi \times \mathcal{L}`$ — how many whole modules a committable build has in place: `Link_p_nom / Link_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | +| $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\Xi \times \mathcal{L}`$ — a bound safely above any feasible flow — the build cap at full availability, data prep | +| $`\mathrm{nonneg}^{f}`$ | `Link_p_min_pu_nonneg` over $`\mathcal{L}`$ — true where none of the link's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | +| $`\mathrm{mnt}^{f}`$ | `Link_maintainable` over $`\mathcal{L}`$ — whether a link must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | +| $`\gamma^{f}`$ | `Link_maintenance_pu` over $`\Xi \times \mathcal{L}`$ — the share of the build a maintenance event takes off | +| $`\mathrm{n}^{f,\mathrm{mnt}}`$ | `Link_maintenance_events` over $`\Xi \times \mathcal{L}`$ — how many maintenance events the horizon holds | +| $`\tau^{f,\mathrm{mnt}}`$ | `Link_maintenance_duration` over $`\Xi \times \mathcal{L}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the link is not maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\mathrm{blk}^{f}`$ | `Link_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the link does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | +| $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\Xi \times \mathcal{J}`$ — nominal internal power | | $`\mathrm{ext}^{z}`$ | `Process_p_nom_extendable` over $`\mathcal{J}`$ — whether the nominal internal power is a decision | -| $`\underline{\mathrm{z}}`$ | `Process_p_min_pu` over $`\mathcal{T} \times \mathcal{J}`$ — least internal power, per unit of nominal power — negative for a process that runs both ways | -| $`\overline{\mathrm{z}}`$ | `Process_p_max_pu` over $`\mathcal{T} \times \mathcal{J}`$ — most internal power, per unit of nominal power | -| $`\alpha`$ | `Process_rate` over $`\mathcal{R}`$ — the energy a port draws or delivers per unit of internal power, PyPSA's `rate0`, `rate1`, … read long — negative where the port withdraws, positive where it injects; a link is a process whose `bus0` rate is minus one and whose output rates are its efficiencies | -| $`\mathrm{d}^{z}`$ | `Process_output_delay` over $`\mathcal{R}`$ — snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that transfers at once | -| $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost | -| $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | -| $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | -| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\mathcal{T} \times \mathcal{J}`$ — a given internal power schedule; a process without one has no row here | -| $`\underline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_min` over $`\mathcal{J}`$ — least nominal power an extendable process may be built at | -| $`\overline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_max` over $`\mathcal{J}`$ — most nominal power an extendable process may be built at | -| $`\mathrm{c}^{\mathrm{cap},z}`$ | `Process_capital_cost` over $`\mathcal{J}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\mathcal{J}`$ — a given nominal power for an extendable process; one without a value has no row here | +| $`\underline{\mathrm{z}}`$ | `Process_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — least internal power, per unit of nominal power — negative for a process that runs both ways | +| $`\overline{\mathrm{z}}`$ | `Process_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most internal power, per unit of nominal power | +| $`\alpha`$ | `Process_rate` over $`\Xi \times \mathcal{R}`$ — the energy a port draws or delivers per unit of internal power, PyPSA's `rate0`, `rate1`, … read long — negative where the port withdraws, positive where it injects; a link is a process whose `bus0` rate is minus one and whose output rates are its efficiencies | +| $`\mathrm{d}^{z}`$ | `Process_output_delay` over $`\mathcal{R}`$ — snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that transfers at once. One for every scenario, as a link's | +| $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost. One for every scenario, as the delay | +| $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | +| $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | +| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\Xi \times \mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — a given internal power schedule; a process without one has no row here | +| $`\underline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_min` over $`\Xi \times \mathcal{J}`$ — least nominal power an extendable process may be built at | +| $`\overline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_max` over $`\Xi \times \mathcal{J}`$ — most nominal power an extendable process may be built at | +| $`\mathrm{c}^{\mathrm{cap},z}`$ | `Process_capital_cost` over $`\Xi \times \mathcal{J}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\Xi \times \mathcal{J}`$ — a given nominal power for an extendable process; one without a value has no row here | | $`\mathrm{com}^{z}`$ | `Process_committable` over $`\mathcal{J}`$ — whether internal power is gated by an on/off status decision | -| $`\mathrm{ru}^{z,\mathrm{up}}`$ | `Process_ramp_limit_start_up` over $`\mathcal{J}`$ — most internal power in the snapshot a process starts, per unit of nominal power | -| $`\mathrm{rd}^{z,\mathrm{dn}}`$ | `Process_ramp_limit_shut_down` over $`\mathcal{J}`$ — most internal power in the snapshot before a process stops, per unit of nominal power | -| $`\mathrm{UT}^{z}`$ | `Process_min_up_time` over $`\mathcal{J}`$ — least snapshots a process stays on once started | -| $`\mathrm{DT}^{z}`$ | `Process_min_down_time` over $`\mathcal{J}`$ — least snapshots a process stays off once stopped | -| $`\mathrm{u}^{z,0}`$ | `Process_status_initial` over $`\mathcal{J}`$ — one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | -| $`\mathrm{hold}^{z}`$ | `Process_must_stay_up` over $`\mathcal{T} \times \mathcal{J}`$ — true while the up time a process brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | -| $`\mathrm{rest}^{z}`$ | `Process_must_stay_down` over $`\mathcal{T} \times \mathcal{J}`$ — true while the down time a process brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | -| $`\mathrm{c}^{z,\mathrm{up}}`$ | `Process_start_up_cost` over $`\mathcal{J}`$ — cost of one start | -| $`\mathrm{c}^{z,\mathrm{dn}}`$ | `Process_shut_down_cost` over $`\mathcal{J}`$ — cost of one stop | -| $`\mathrm{c}^{z,\mathrm{on}}`$ | `Process_stand_by_cost` over $`\mathcal{T} \times \mathcal{J}`$ — cost of one snapshot spent on | +| $`\mathrm{ru}^{z,\mathrm{up}}`$ | `Process_ramp_limit_start_up` over $`\Xi \times \mathcal{J}`$ — most internal power in the snapshot a process starts, per unit of nominal power | +| $`\mathrm{rd}^{z,\mathrm{dn}}`$ | `Process_ramp_limit_shut_down` over $`\Xi \times \mathcal{J}`$ — most internal power in the snapshot before a process stops, per unit of nominal power | +| $`\mathrm{UT}^{z}`$ | `Process_min_up_time` over $`\Xi \times \mathcal{J}`$ — least snapshots a process stays on once started | +| $`\mathrm{DT}^{z}`$ | `Process_min_down_time` over $`\Xi \times \mathcal{J}`$ — least snapshots a process stays off once stopped | +| $`\mathrm{u}^{z,0}`$ | `Process_status_initial` over $`\Xi \times \mathcal{J}`$ — one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{hold}^{z}`$ | `Process_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true while the up time a process brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | +| $`\mathrm{rest}^{z}`$ | `Process_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true while the down time a process brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | +| $`\mathrm{c}^{z,\mathrm{up}}`$ | `Process_start_up_cost` over $`\Xi \times \mathcal{J}`$ — cost of one start | +| $`\mathrm{c}^{z,\mathrm{dn}}`$ | `Process_shut_down_cost` over $`\Xi \times \mathcal{J}`$ — cost of one stop | +| $`\mathrm{c}^{z,\mathrm{on}}`$ | `Process_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one snapshot spent on | | $`\mathrm{z}^{\mathrm{mod}}`$ | `Process_p_nom_mod` over $`\mathcal{J}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | -| $`\mathrm{N}^{z,\mathrm{fix}}`$ | `Process_modules_installed` over $`\mathcal{J}`$ — how many whole modules a committable build has in place: `Process_p_nom / Process_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | -| $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\mathcal{J}`$ — a bound safely above any feasible internal power — the build cap at full availability, data prep | -| $`\mathrm{nonneg}^{z}`$ | `Process_p_min_pu_nonneg` over $`\mathcal{J}`$ — true where none of the process's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep | -| $`\mathrm{mnt}^{z}`$ | `Process_maintainable` over $`\mathcal{J}`$ — whether a process must be taken off for maintenance within the horizon | -| $`\gamma^{z}`$ | `Process_maintenance_pu` over $`\mathcal{J}`$ — the share of the build a maintenance event takes off | -| $`\mathrm{n}^{z,\mathrm{mnt}}`$ | `Process_maintenance_events` over $`\mathcal{J}`$ — how many maintenance events the horizon holds | -| $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\mathcal{J}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | -| $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | +| $`\mathrm{N}^{z,\mathrm{fix}}`$ | `Process_modules_installed` over $`\Xi \times \mathcal{J}`$ — how many whole modules a committable build has in place: `Process_p_nom / Process_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | +| $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\Xi \times \mathcal{J}`$ — a bound safely above any feasible internal power — the build cap at full availability, data prep | +| $`\mathrm{nonneg}^{z}`$ | `Process_p_min_pu_nonneg` over $`\mathcal{J}`$ — true where none of the process's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | +| $`\mathrm{mnt}^{z}`$ | `Process_maintainable` over $`\mathcal{J}`$ — whether a process must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | +| $`\gamma^{z}`$ | `Process_maintenance_pu` over $`\Xi \times \mathcal{J}`$ — the share of the build a maintenance event takes off | +| $`\mathrm{n}^{z,\mathrm{mnt}}`$ | `Process_maintenance_events` over $`\Xi \times \mathcal{J}`$ — how many maintenance events the horizon holds | +| $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\Xi \times \mathcal{J}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | @@ -3618,115 +3761,115 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{new}^{e}`$ | `Store_first_active` over $`\mathcal{Y} \times \mathcal{V}`$ — one in the first period a store stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | | $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | | $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit | -| $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top | -| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | -| $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\mathcal{T} \times \mathcal{L}`$ — a given flow schedule; a link without one has no row here | +| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:226-230`), data prep | +| $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top — the least over the scenarios, as PyPSA takes it, data prep | +| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | +| $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — a given flow schedule; a link without one has no row here | | $`\mathrm{w}^{\mathrm{sto}}`$ | `snapshot_weightings_stores` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.stores` — hours a snapshot stands for in a storage balance | | $`\mathrm{w}^{\mathrm{gen}}`$ | `snapshot_weightings_generators` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.generators` — hours a snapshot stands for in an energy total | -| $`\underline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_min` over $`\mathcal{G}`$ — least nominal power an extendable generator may be built at | -| $`\overline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_max` over $`\mathcal{G}`$ — most nominal power an extendable generator may be built at | -| $`\mathrm{c}^{\mathrm{cap}}`$ | `Generator_capital_cost` over $`\mathcal{G}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\mathcal{G}`$ — a given nominal power for an extendable generator; one without a value has no row here | -| $`\underline{\mathrm{E}}`$ | `Generator_e_sum_min` over $`\mathcal{G}`$ — least energy over the horizon; minus infinity where no floor is meant | -| $`\overline{\mathrm{E}}`$ | `Generator_e_sum_max` over $`\mathcal{G}`$ — most energy over the horizon — a fuel or emission budget in energy terms; infinity where no cap is meant | -| $`\underline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_min` over $`\mathcal{L}`$ — least nominal power an extendable link may be built at | -| $`\overline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_max` over $`\mathcal{L}`$ — most nominal power an extendable link may be built at | -| $`\mathrm{c}^{\mathrm{cap},f}`$ | `Link_capital_cost` over $`\mathcal{L}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\mathrm{f}^{\mathrm{nom,set}}`$ | `Link_p_nom_set` over $`\mathcal{L}`$ — a given nominal power for an extendable link; one without a value has no row here | -| $`\underline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_min` over $`\mathcal{S}`$ — least nominal power an extendable storage unit may be built at | -| $`\overline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_max` over $`\mathcal{S}`$ — most nominal power an extendable storage unit may be built at | -| $`\mathrm{c}^{\mathrm{cap},h}`$ | `StorageUnit_capital_cost` over $`\mathcal{S}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\mathrm{h}^{\mathrm{nom,set}}`$ | `StorageUnit_p_nom_set` over $`\mathcal{S}`$ — a given nominal power for an extendable storage unit; one without a value has no row here | -| $`\underline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_min` over $`\mathcal{V}`$ — least nominal capacity an extendable store may be built at | -| $`\overline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_max` over $`\mathcal{V}`$ — most nominal capacity an extendable store may be built at | -| $`\mathrm{c}^{\mathrm{cap},e}`$ | `Store_capital_cost` over $`\mathcal{V}`$ — cost of one unit of nominal capacity — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\mathrm{e}^{\mathrm{nom,set}}`$ | `Store_e_nom_set` over $`\mathcal{V}`$ — a given nominal capacity for an extendable store; one without a value has no row here | -| $`\mathrm{h}^{\mathrm{nom}}`$ | `StorageUnit_p_nom` over $`\mathcal{S}`$ — nominal power | +| $`\underline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_min` over $`\Xi \times \mathcal{G}`$ — least nominal power an extendable generator may be built at | +| $`\overline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_max` over $`\Xi \times \mathcal{G}`$ — most nominal power an extendable generator may be built at | +| $`\mathrm{c}^{\mathrm{cap}}`$ | `Generator_capital_cost` over $`\Xi \times \mathcal{G}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\Xi \times \mathcal{G}`$ — a given nominal power for an extendable generator; one without a value has no row here | +| $`\underline{\mathrm{E}}`$ | `Generator_e_sum_min` over $`\Xi \times \mathcal{G}`$ — least energy over the horizon; minus infinity where no floor is meant | +| $`\overline{\mathrm{E}}`$ | `Generator_e_sum_max` over $`\Xi \times \mathcal{G}`$ — most energy over the horizon — a fuel or emission budget in energy terms; infinity where no cap is meant | +| $`\underline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_min` over $`\Xi \times \mathcal{L}`$ — least nominal power an extendable link may be built at | +| $`\overline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_max` over $`\Xi \times \mathcal{L}`$ — most nominal power an extendable link may be built at | +| $`\mathrm{c}^{\mathrm{cap},f}`$ | `Link_capital_cost` over $`\Xi \times \mathcal{L}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\mathrm{f}^{\mathrm{nom,set}}`$ | `Link_p_nom_set` over $`\Xi \times \mathcal{L}`$ — a given nominal power for an extendable link; one without a value has no row here | +| $`\underline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_min` over $`\Xi \times \mathcal{S}`$ — least nominal power an extendable storage unit may be built at | +| $`\overline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_max` over $`\Xi \times \mathcal{S}`$ — most nominal power an extendable storage unit may be built at | +| $`\mathrm{c}^{\mathrm{cap},h}`$ | `StorageUnit_capital_cost` over $`\Xi \times \mathcal{S}`$ — cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\mathrm{h}^{\mathrm{nom,set}}`$ | `StorageUnit_p_nom_set` over $`\Xi \times \mathcal{S}`$ — a given nominal power for an extendable storage unit; one without a value has no row here | +| $`\underline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_min` over $`\Xi \times \mathcal{V}`$ — least nominal capacity an extendable store may be built at | +| $`\overline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_max` over $`\Xi \times \mathcal{V}`$ — most nominal capacity an extendable store may be built at | +| $`\mathrm{c}^{\mathrm{cap},e}`$ | `Store_capital_cost` over $`\Xi \times \mathcal{V}`$ — cost of one unit of nominal capacity — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\mathrm{e}^{\mathrm{nom,set}}`$ | `Store_e_nom_set` over $`\Xi \times \mathcal{V}`$ — a given nominal capacity for an extendable store; one without a value has no row here | +| $`\mathrm{h}^{\mathrm{nom}}`$ | `StorageUnit_p_nom` over $`\Xi \times \mathcal{S}`$ — nominal power | | $`\mathrm{ext}^{h}`$ | `StorageUnit_p_nom_extendable` over $`\mathcal{S}`$ — whether the nominal power is a decision | -| $`\underline{\mathrm{h}}`$ | `StorageUnit_p_min_pu` over $`\mathcal{T} \times \mathcal{S}`$ — most storing, per unit of nominal power and negated | -| $`\overline{\mathrm{h}}`$ | `StorageUnit_p_max_pu` over $`\mathcal{T} \times \mathcal{S}`$ — most dispatch, per unit of nominal power | -| $`\mathrm{T}^{h}`$ | `StorageUnit_max_hours` over $`\mathcal{S}`$ — energy capacity, as hours of dispatch at nominal power | -| $`\eta^{-}`$ | `StorageUnit_efficiency_store` over $`\mathcal{S}`$ — share of the power drawn from the bus that becomes charge | -| $`\eta^{+}`$ | `StorageUnit_efficiency_dispatch` over $`\mathcal{S}`$ — share of the charge drawn down that reaches the bus | -| $`\rho`$ | `StorageUnit_retention` over $`\mathcal{T} \times \mathcal{S}`$ — share of charge kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep | -| $`\mathrm{inflow}`$ | `StorageUnit_inflow` over $`\mathcal{T} \times \mathcal{S}`$ — energy arriving per hour, a river into a reservoir | -| $`\mathrm{soc}^{0}`$ | `StorageUnit_state_of_charge_initial` over $`\mathcal{S}`$ — charge held before the first snapshot | -| $`\mathrm{cyc}`$ | `StorageUnit_cyclic_state_of_charge` over $`\mathcal{S}`$ — whether the horizon closes on itself instead of opening on the initial charge | -| $`\mathrm{cyc}^{y}`$ | `StorageUnit_cyclic_state_of_charge_per_period` over $`\mathcal{S}`$ — whether each investment period closes on itself instead of carrying its charge on to the next; it overrides `cyclic_state_of_charge` and `state_of_charge_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | -| $`\mathrm{reset}`$ | `StorageUnit_state_of_charge_initial_per_period` over $`\mathcal{S}`$ — whether each investment period opens on the initial charge instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\underline{\mathrm{h}}`$ | `StorageUnit_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — most storing, per unit of nominal power and negated | +| $`\overline{\mathrm{h}}`$ | `StorageUnit_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — most dispatch, per unit of nominal power | +| $`\mathrm{T}^{h}`$ | `StorageUnit_max_hours` over $`\Xi \times \mathcal{S}`$ — energy capacity, as hours of dispatch at nominal power | +| $`\eta^{-}`$ | `StorageUnit_efficiency_store` over $`\Xi \times \mathcal{S}`$ — share of the power drawn from the bus that becomes charge | +| $`\eta^{+}`$ | `StorageUnit_efficiency_dispatch` over $`\Xi \times \mathcal{S}`$ — share of the charge drawn down that reaches the bus | +| $`\rho`$ | `StorageUnit_retention` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — share of charge kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep | +| $`\mathrm{inflow}`$ | `StorageUnit_inflow` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — energy arriving per hour, a river into a reservoir | +| $`\mathrm{soc}^{0}`$ | `StorageUnit_state_of_charge_initial` over $`\Xi \times \mathcal{S}`$ — charge held before the first snapshot | +| $`\mathrm{cyc}`$ | `StorageUnit_cyclic_state_of_charge` over $`\Xi \times \mathcal{S}`$ — whether the horizon closes on itself instead of opening on the initial charge | +| $`\mathrm{cyc}^{y}`$ | `StorageUnit_cyclic_state_of_charge_per_period` over $`\Xi \times \mathcal{S}`$ — whether each investment period closes on itself instead of carrying its charge on to the next; it overrides `cyclic_state_of_charge` and `state_of_charge_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\mathrm{reset}`$ | `StorageUnit_state_of_charge_initial_per_period` over $`\Xi \times \mathcal{S}`$ — whether each investment period opens on the initial charge instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{open}`$ | `StorageUnit_opens_late` over $`\mathcal{T} \times \mathcal{S}`$ — whether a snapshot is the first a storage unit stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every unit stands throughout | | $`\mathrm{idle}`$ | `StorageUnit_inactive_snapshots` over $`\mathcal{S}`$ — how many snapshots a storage unit does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic unit reaches back this many snapshots further, so it closes on the last snapshot it stands in | -| $`\mathrm{c}^{h}`$ | `StorageUnit_marginal_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of dispatch | -| $`\mathrm{c}^{h,(2)}`$ | `StorageUnit_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{S}`$ — cost of the square of one unit of dispatch; storing is not charged | -| $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | -| $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | -| $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given net dispatch schedule; a unit without one has no row here | -| $`\mathrm{h}^{+,\mathrm{set}}`$ | `StorageUnit_p_dispatch_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given dispatch schedule; a unit without one has no row here | -| $`\mathrm{h}^{-,\mathrm{set}}`$ | `StorageUnit_p_store_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given charging schedule; a unit without one has no row here | -| $`\mathrm{soc}^{\mathrm{set}}`$ | `StorageUnit_state_of_charge_set` over $`\mathcal{T} \times \mathcal{S}`$ — a given charge schedule; a unit without one has no row here | -| $`\mathrm{e}^{\mathrm{nom}}`$ | `Store_e_nom` over $`\mathcal{V}`$ — nominal energy capacity | +| $`\mathrm{c}^{h}`$ | `StorageUnit_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of dispatch | +| $`\mathrm{c}^{h,(2)}`$ | `StorageUnit_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of the square of one unit of dispatch; storing is not charged | +| $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | +| $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | +| $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given net dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{+,\mathrm{set}}`$ | `StorageUnit_p_dispatch_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{-,\mathrm{set}}`$ | `StorageUnit_p_store_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given charging schedule; a unit without one has no row here | +| $`\mathrm{soc}^{\mathrm{set}}`$ | `StorageUnit_state_of_charge_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given charge schedule; a unit without one has no row here | +| $`\mathrm{e}^{\mathrm{nom}}`$ | `Store_e_nom` over $`\Xi \times \mathcal{V}`$ — nominal energy capacity | | $`\mathrm{ext}^{e}`$ | `Store_e_nom_extendable` over $`\mathcal{V}`$ — whether the nominal energy capacity is a decision | -| $`\underline{\mathrm{e}}`$ | `Store_e_min_pu` over $`\mathcal{T} \times \mathcal{V}`$ — least energy held, per unit of nominal capacity — negative for a store that may go short | -| $`\overline{\mathrm{e}}`$ | `Store_e_max_pu` over $`\mathcal{T} \times \mathcal{V}`$ — most energy held, per unit of nominal capacity | -| $`\rho^{e}`$ | `Store_retention` over $`\mathcal{T} \times \mathcal{V}`$ — share of energy kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep | -| $`\mathrm{e}^{0}`$ | `Store_e_initial` over $`\mathcal{V}`$ — energy held before the first snapshot | -| $`\mathrm{cyc}^{e}`$ | `Store_e_cyclic` over $`\mathcal{V}`$ — whether the horizon closes on itself instead of opening on the initial energy | -| $`\mathrm{cyc}^{e,y}`$ | `Store_e_cyclic_per_period` over $`\mathcal{V}`$ — whether each investment period closes on itself instead of carrying its energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | -| $`\mathrm{reset}^{e}`$ | `Store_e_initial_per_period` over $`\mathcal{V}`$ — whether each investment period opens on the initial energy instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\underline{\mathrm{e}}`$ | `Store_e_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — least energy held, per unit of nominal capacity — negative for a store that may go short | +| $`\overline{\mathrm{e}}`$ | `Store_e_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — most energy held, per unit of nominal capacity | +| $`\rho^{e}`$ | `Store_retention` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — share of energy kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep | +| $`\mathrm{e}^{0}`$ | `Store_e_initial` over $`\Xi \times \mathcal{V}`$ — energy held before the first snapshot | +| $`\mathrm{cyc}^{e}`$ | `Store_e_cyclic` over $`\Xi \times \mathcal{V}`$ — whether the horizon closes on itself instead of opening on the initial energy | +| $`\mathrm{cyc}^{e,y}`$ | `Store_e_cyclic_per_period` over $`\Xi \times \mathcal{V}`$ — whether each investment period closes on itself instead of carrying its energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | +| $`\mathrm{reset}^{e}`$ | `Store_e_initial_per_period` over $`\Xi \times \mathcal{V}`$ — whether each investment period opens on the initial energy instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{open}^{e}`$ | `Store_opens_late` over $`\mathcal{T} \times \mathcal{V}`$ — whether a snapshot is the first a store stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every store stands throughout | | $`\mathrm{idle}^{e}`$ | `Store_inactive_snapshots` over $`\mathcal{V}`$ — how many snapshots a store does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic store reaches back this many snapshots further, so it closes on the last snapshot it stands in | -| $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | -| $`\mathrm{c}^{q,(2)}`$ | `Store_marginal_cost_quadratic` over $`\mathcal{T} \times \mathcal{V}`$ — cost of the square of the net power delivered, so charging costs as much as delivering | -| $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | -| $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | -| $`\mathrm{q}^{\mathrm{set}}`$ | `Store_p_set` over $`\mathcal{T} \times \mathcal{V}`$ — a given schedule of power delivered; a store without one has no row here | -| $`\mathrm{s}^{\mathrm{nom}}`$ | `Line_s_nom` over $`\mathcal{K}`$ — nominal apparent power | +| $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | +| $`\mathrm{c}^{q,(2)}`$ | `Store_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of the square of the net power delivered, so charging costs as much as delivering | +| $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | +| $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | +| $`\mathrm{q}^{\mathrm{set}}`$ | `Store_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — a given schedule of power delivered; a store without one has no row here | +| $`\mathrm{s}^{\mathrm{nom}}`$ | `Line_s_nom` over $`\Xi \times \mathcal{K}`$ — nominal apparent power | | $`\mathrm{ext}^{s}`$ | `Line_s_nom_extendable` over $`\mathcal{K}`$ — whether the nominal apparent power is a decision | -| $`\overline{\mathrm{s}}`$ | `Line_s_max_pu` over $`\mathcal{T} \times \mathcal{K}`$ — most flow either way, per unit of nominal apparent power | -| $`\underline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_min` over $`\mathcal{K}`$ — least nominal apparent power an extendable line may be built at | -| $`\overline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_max` over $`\mathcal{K}`$ — most nominal apparent power an extendable line may be built at | -| $`\mathrm{c}^{\mathrm{cap},s}`$ | `Line_capital_cost` over $`\mathcal{K}`$ — cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | -| $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | -| $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row | +| $`\overline{\mathrm{s}}`$ | `Line_s_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — most flow either way, per unit of nominal apparent power | +| $`\underline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_min` over $`\Xi \times \mathcal{K}`$ — least nominal apparent power an extendable line may be built at | +| $`\overline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_max` over $`\Xi \times \mathcal{K}`$ — most nominal apparent power an extendable line may be built at | +| $`\mathrm{c}^{\mathrm{cap},s}`$ | `Line_capital_cost` over $`\Xi \times \mathcal{K}`$ — cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\Xi \times \mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | +| $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | +| $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1354-1361`) | | $`\beta`$ | `Line_BODF` over $`\mathcal{K} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a line takes on when that branch goes out — PyPSA's `BODF`, from the sub-network's PTDF, data prep; a row only where the line and the outage share a sub-network, -1 at the outaged line itself | | $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the same; false with no segments is a lossless run | -| $`\overline{\ell}`$ | `Line_loss_max` over $`\mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | -| $`\mathrm{a}`$ | `Line_loss_slope` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep | -| $`\mathrm{b}`$ | `Line_loss_offset` over $`\mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | -| $`\sigma^{\mathrm{nom}}`$ | `Transformer_s_nom` over $`\mathcal{M}`$ — nominal apparent power | +| $`\overline{\ell}`$ | `Line_loss_max` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | +| $`\mathrm{a}`$ | `Line_loss_slope` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep | +| $`\mathrm{b}`$ | `Line_loss_offset` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | +| $`\sigma^{\mathrm{nom}}`$ | `Transformer_s_nom` over $`\Xi \times \mathcal{M}`$ — nominal apparent power | | $`\mathrm{ext}^{\sigma}`$ | `Transformer_s_nom_extendable` over $`\mathcal{M}`$ — whether the nominal apparent power is a decision | -| $`\overline{\sigma}`$ | `Transformer_s_max_pu` over $`\mathcal{T} \times \mathcal{M}`$ — most flow either way, per unit of nominal apparent power | -| $`\underline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_min` over $`\mathcal{M}`$ — least nominal apparent power an extendable transformer may be built at | -| $`\overline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_max` over $`\mathcal{M}`$ — most nominal apparent power an extendable transformer may be built at | -| $`\mathrm{c}^{\mathrm{cap},\sigma}`$ | `Transformer_capital_cost` over $`\mathcal{M}`$ — cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep | -| $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$ — a given nominal apparent power for an extendable transformer; one without a value has no row here | -| $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | -| $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row | +| $`\overline{\sigma}`$ | `Transformer_s_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — most flow either way, per unit of nominal apparent power | +| $`\underline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_min` over $`\Xi \times \mathcal{M}`$ — least nominal apparent power an extendable transformer may be built at | +| $`\overline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_max` over $`\Xi \times \mathcal{M}`$ — most nominal apparent power an extendable transformer may be built at | +| $`\mathrm{c}^{\mathrm{cap},\sigma}`$ | `Transformer_capital_cost` over $`\Xi \times \mathcal{M}`$ — cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep | +| $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\Xi \times \mathcal{M}`$ — a given nominal apparent power for an extendable transformer; one without a value has no row here | +| $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | +| $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's | | $`\beta^{\sigma}`$ | `Transformer_BODF` over $`\mathcal{M} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage share a sub-network | | $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{M} \times \mathcal{C}`$ — a fixed transformer's phase shift in radians, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; zero for a varying transformer, whose shift is a decision instead, so the constant and the variable term never both count a shift. A transformer with no shift or in no cycle has no row | -| $`\mathrm{Transformer\_phase\_shift\_varying}`$ | `Transformer_phase_shift_varying` over $`\mathcal{M}`$ — whether a transformer's phase shift is a decision — PyPSA's `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a fixed shift carried by `phase_shift` | +| $`\mathrm{Transformer\_phase\_shift\_varying}`$ | `Transformer_phase_shift_varying` over $`\mathcal{M}`$ — whether a transformer's phase shift is a decision — PyPSA's `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a fixed shift carried by `phase_shift`. The shift parameters carry no scenario: only a cycle row reads them, and PyPSA fails on a transformer in a cycle on a network with scenarios (`constraints.py:1654`) | | $`\mathrm{Transformer\_phase\_shift\_min}`$ | `Transformer_phase_shift_min` over $`\mathcal{M}`$ — the least a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_min`; where it is below `phase_shift_max` the shift is a decision, otherwise the transformer keeps its fixed `phase_shift` | | $`\mathrm{Transformer\_phase\_shift\_max}`$ | `Transformer_phase_shift_max` over $`\mathcal{M}`$ — the most a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_max`; equal to `phase_shift_min` for a fixed transformer | | $`\mathrm{Transformer\_phase\_shift\_cycle\_weight}`$ | `Transformer_phase_shift_cycle_weight` over $`\mathcal{M} \times \mathcal{C}`$ — the cycle sign for a varying transformer's phase shift, times π/180 so a shift in degrees enters the cycle sum in radians — data prep; zero for a fixed transformer or one in no cycle | -| $`\overline{\ell}^{\sigma}`$ | `Transformer_loss_max` over $`\mathcal{T} \times \mathcal{M}`$ — the loss at a transformer's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, its `r_pu_eff` the resistance over the given `s_nom` times the tap ratio, data prep | -| $`\mathrm{a}^{\sigma}`$ | `Transformer_loss_slope` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — the slope of a cut to a transformer's loss curve — a tangent's `2 * r_pu_eff * p_k`, a secant's `r_pu_eff * (p_k + p_k+1)`, as a line's, over the transformer's own `r_pu_eff` and rating, data prep | -| $`\mathrm{b}^{\sigma}`$ | `Transformer_loss_offset` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | +| $`\overline{\ell}^{\sigma}`$ | `Transformer_loss_max` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — the loss at a transformer's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, its `r_pu_eff` the resistance over the given `s_nom` times the tap ratio, data prep | +| $`\mathrm{a}^{\sigma}`$ | `Transformer_loss_slope` over $`\Xi \times \mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — the slope of a cut to a transformer's loss curve — a tangent's `2 * r_pu_eff * p_k`, a secant's `r_pu_eff * (p_k + p_k+1)`, as a line's, over the transformer's own `r_pu_eff` and rating, data prep | +| $`\mathrm{b}^{\sigma}`$ | `Transformer_loss_offset` over $`\Xi \times \mathcal{T} \times \mathcal{M} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | | $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{I}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | | $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\Xi \times \mathcal{I}`$ — which way the row binds in each scenario — `<=`, `>=` or `==`; PyPSA reads a row's sense per scenario (`global_constraints.py:556`, `:748`, `:860`) | | $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\Xi \times \mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, times its period's years for each counted period where the storage reopens per period, or a non-extendable build — is folded in here by data prep. PyPSA reads it per scenario (`global_constraints.py:557`, `:749`, `:861`) | -| $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\mathcal{I} \times \mathcal{T}`$ — whether a row counts a snapshot — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:377`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:375`) | -| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\mathcal{I} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency, data prep; a generator of an unweighted carrier has no row | -| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\mathcal{I} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | -| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\mathcal{I} \times \mathcal{V}`$ — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | -| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row | -| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's set — data prep; one outside it has no row | -| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's set — data prep; one outside it has no row | -| $`\mathrm{len}`$ | `Line_volume_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's length where its carrier is in the row's set — data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's length where its carrier is in the row's set — data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row | -| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\mathcal{I} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row | +| $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\Xi \times \mathcal{I} \times \mathcal{T}`$ — whether a row counts a snapshot in a scenario — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:377`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:375`) | +| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency, data prep; a generator of an unweighted carrier has no row | +| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | +| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{V}`$ — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | +| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row | +| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's set — data prep; one outside it has no row | +| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's set — data prep; one outside it has no row | +| $`\mathrm{len}`$ | `Line_volume_weight` over $`\Xi \times \mathcal{I} \times \mathcal{K}`$ — the line's length where its carrier is in the row's set, the first scenario's length as PyPSA reads it (`global_constraints.py:835-836`) — data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\Xi \times \mathcal{I} \times \mathcal{L}`$ — the link's length where its carrier is in the row's set, the first scenario's length as PyPSA reads it (`global_constraints.py:835-836`) — data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\Xi \times \mathcal{I} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row | +| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\Xi \times \mathcal{I} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row | | $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | | $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{L}`$ — one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | | $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | @@ -3793,46 +3936,46 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file |---|---| | $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | -| $`\widetilde{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_effective` over $`\mathcal{G}`$ — the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise | -| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | -| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | -| $`\widetilde{\mathrm{ru}}^{\mathrm{up}}`$ | `Generator_start_up_rate` over $`\mathcal{G}`$ — the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | -| $`\widetilde{\mathrm{rd}}^{\mathrm{dn}}`$ | `Generator_shut_down_rate` over $`\mathcal{G}`$ — the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | -| $`\widehat{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_committed` over $`\mathcal{G}`$ — the build a committed unit's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | +| $`\widetilde{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_effective` over $`\Xi \times \mathcal{G}`$ — the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise | +| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\Xi \times \mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\Xi \times \mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{\mathrm{up}}`$ | `Generator_start_up_rate` over $`\Xi \times \mathcal{G}`$ — the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | +| $`\widetilde{\mathrm{rd}}^{\mathrm{dn}}`$ | `Generator_shut_down_rate` over $`\Xi \times \mathcal{G}`$ — the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | +| $`\widehat{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_committed` over $`\Xi \times \mathcal{G}`$ — the build a committed unit's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{+}`$ | `Generator_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | -| $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | +| $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\Xi \times \mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{f}`$ | `Link_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the commitment state a link carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{f}`$ | `Link_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the flow a link carries into a snapshot — nothing at the start of the horizon, which is why a link that came in running carries no ramp row there | -| $`\widetilde{\mathrm{ru}}^{f}`$ | `Link_ramp_up_rate` over $`\mathcal{L}`$ — the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | -| $`\widetilde{\mathrm{rd}}^{f}`$ | `Link_ramp_down_rate` over $`\mathcal{L}`$ — the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | -| $`\widetilde{\mathrm{ru}}^{f,\mathrm{up}}`$ | `Link_start_up_rate` over $`\mathcal{L}`$ — the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | -| $`\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}`$ | `Link_shut_down_rate` over $`\mathcal{L}`$ — the shut-down ramp a link's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | -| $`\widehat{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_committed` over $`\mathcal{L}`$ — the build a committed link's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | +| $`\widetilde{\mathrm{ru}}^{f}`$ | `Link_ramp_up_rate` over $`\Xi \times \mathcal{L}`$ — the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}^{f}`$ | `Link_ramp_down_rate` over $`\Xi \times \mathcal{L}`$ — the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{f,\mathrm{up}}`$ | `Link_start_up_rate` over $`\Xi \times \mathcal{L}`$ — the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | +| $`\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}`$ | `Link_shut_down_rate` over $`\Xi \times \mathcal{L}`$ — the shut-down ramp a link's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | +| $`\widehat{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_committed` over $`\Xi \times \mathcal{L}`$ — the build a committed link's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{f,+}`$ | `Link_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may raise flow between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{f,-}`$ | `Link_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may lower flow between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | -| $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | +| $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\Xi \times \mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{z}`$ | `Process_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the commitment state a process carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{z}`$ | `Process_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the internal power a process carries into a snapshot — nothing at the start of the horizon, which is why a process that came in running carries no ramp row there | -| $`\widetilde{\mathrm{ru}}^{z}`$ | `Process_ramp_up_rate` over $`\mathcal{J}`$ — the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | -| $`\widetilde{\mathrm{rd}}^{z}`$ | `Process_ramp_down_rate` over $`\mathcal{J}`$ — the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | -| $`\widetilde{\mathrm{ru}}^{z,\mathrm{up}}`$ | `Process_start_up_rate` over $`\mathcal{J}`$ — the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | -| $`\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}`$ | `Process_shut_down_rate` over $`\mathcal{J}`$ — the shut-down ramp a process's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | -| $`\widehat{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_committed` over $`\mathcal{J}`$ — the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | +| $`\widetilde{\mathrm{ru}}^{z}`$ | `Process_ramp_up_rate` over $`\Xi \times \mathcal{J}`$ — the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}^{z}`$ | `Process_ramp_down_rate` over $`\Xi \times \mathcal{J}`$ — the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{z,\mathrm{up}}`$ | `Process_start_up_rate` over $`\Xi \times \mathcal{J}`$ — the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | +| $`\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}`$ | `Process_shut_down_rate` over $`\Xi \times \mathcal{J}`$ — the shut-down ramp a process's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | +| $`\widehat{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_committed` over $`\Xi \times \mathcal{J}`$ — the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | | $`\Delta^{z,+}`$ | `Process_ramp_up_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may raise internal power between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | | $`\Delta^{z,-}`$ | `Process_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — how far a process may lower internal power between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\overleftarrow{\mathit{soc}}`$ | `StorageUnit_charge_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — the charge a unit opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial charge, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A unit built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon | | $`\overleftarrow{e}`$ | `Store_energy_carried_in` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — the energy a store opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial energy, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A store built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon | | $`\overrightarrow{f}`$ | `Link_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{O}`$ — what a link delivers to an output port at a snapshot — its flow after the port's efficiency, delayed by the port's `delay` within its investment period; where the port is `cyclic_delay` the delayed flow wraps from the period's end, and where it is not the flow still in transit at the period's first snapshots is lost. A port that does not delay (`delay` zero) delivers its flow unshifted, cyclic or not | | $`\overrightarrow{z}`$ | `Process_output_arrival` over $`\Xi \times \mathcal{T} \times \mathcal{R}`$ — what a process transfers at a port at a snapshot — its internal power times the port's rate, delayed by the port's `delay` within its investment period; where the port is `cyclic_delay` the delayed transfer wraps from the period's end, and where it is not the energy still in transit at the period's first snapshots is lost. A port that does not delay (`delay` zero) transfers at once, cyclic or not | -| $`\mathit{w}^{\mathrm{gc}}`$ | `GlobalConstraint_energy_weight` over $`\mathcal{I} \times \mathcal{T}`$ — what one unit of power at a snapshot counts for in a row — the generator weighting times the years of the snapshot's period, where the row counts the snapshot, and nothing where it does not | -| $`\mathit{last}`$ | `GlobalConstraint_snapshot_closes` over $`\mathcal{I} \times \mathcal{T}`$ — one at the last snapshot a row counts, and zero elsewhere | -| $`\mathit{w}^{h}`$ | `StorageUnit_closing_weight` over $`\mathcal{I} \times \mathcal{T} \times \mathcal{S}`$ — what the charge a unit holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the unit reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere | -| $`\mathit{w}^{e}`$ | `Store_closing_weight` over $`\mathcal{I} \times \mathcal{T} \times \mathcal{V}`$ — what the energy a store holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the store reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere | +| $`\mathit{w}^{\mathrm{gc}}`$ | `GlobalConstraint_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{T}`$ — what one unit of power at a snapshot counts for in a row — the generator weighting times the years of the snapshot's period, where the row counts the snapshot, and nothing where it does not | +| $`\mathit{last}`$ | `GlobalConstraint_snapshot_closes` over $`\Xi \times \mathcal{I} \times \mathcal{T}`$ — one at the last snapshot a row counts, and zero elsewhere | +| $`\mathit{w}^{h}`$ | `StorageUnit_closing_weight` over $`\Xi \times \mathcal{I} \times \mathcal{T} \times \mathcal{S}`$ — what the charge a unit holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the unit reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere | +| $`\mathit{w}^{e}`$ | `Store_closing_weight` over $`\Xi \times \mathcal{I} \times \mathcal{T} \times \mathcal{V}`$ — what the energy a store holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the store reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere | | $`\mathit{primary\_energy}`$ | `primary_energy` over $`\Xi \times \mathcal{I}`$ — what a `primary_energy` row totals — weighted generator energy over the snapshots it counts, less the charge left in weighted storage at the close; the initial charge it is compared against is folded into the row's constant | | $`\mathit{operational\_limit}`$ | `operational_limit` over $`\Xi \times \mathcal{I}`$ — what an `operational_limit` row totals — the weighted energy its generators deliver over the snapshots it counts, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant | -| $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\mathcal{I}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | -| $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\mathcal{I}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | +| $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\Xi \times \mathcal{I}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | +| $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\Xi \times \mathcal{I}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | | $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. Like PyPSA, it sums only the components that carry a carrier attribute, so a transformer, which has none, counts in no carrier | @@ -3860,21 +4003,23 @@ $`\lvert \mathcal{T} \rvert`$ denotes the size of the set being counted along, a ```yaml objective: sense: minimize - description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail + description: >- + capacity once per active period at its expected cost over the scenarios, operation in + expectation over the scenarios, and a share of it at the tail expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) - + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) - + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) - + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) - + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) - + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + sum(scenario_weight * Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(scenario_weight * Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(scenario_weight * StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(scenario_weight * Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(scenario_weight * Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(scenario_weight * Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(scenario_weight * Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + CVaR_omega * CVaR ``` ```math -\min \sum_{g \in \mathcal{G}} P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{g} \cdot \mathrm{W}_{g} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{l} \cdot \mathrm{W}^{f}_{l} + \sum_{s \in \mathcal{S}} H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{s} \cdot \mathrm{W}^{h}_{s} + \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{v} \cdot \mathrm{W}^{e}_{v} + \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{k} \cdot \mathrm{W}^{s}_{k} + \sum_{j \in \mathcal{J}} Z_{j} \cdot \mathrm{c}^{\mathrm{cap},z}_{j} \cdot \mathrm{W}^{z}_{j} + \sum_{m \in \mathcal{M}} \Sigma_{m} \cdot \mathrm{c}^{\mathrm{cap},\sigma}_{m} \cdot \mathrm{W}^{\sigma}_{m} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR +\min \sum_{\xi \in \Xi,\ g \in \mathcal{G}} \pi_{\xi} \cdot P_{g} \cdot \mathrm{c}^{\mathrm{cap}}_{\xi,g} \cdot \mathrm{W}_{g} + \sum_{\xi \in \Xi,\ l \in \mathcal{L}} \pi_{\xi} \cdot F_{l} \cdot \mathrm{c}^{\mathrm{cap},f}_{\xi,l} \cdot \mathrm{W}^{f}_{l} + \sum_{\xi \in \Xi,\ s \in \mathcal{S}} \pi_{\xi} \cdot H_{s} \cdot \mathrm{c}^{\mathrm{cap},h}_{\xi,s} \cdot \mathrm{W}^{h}_{s} + \sum_{\xi \in \Xi,\ v \in \mathcal{V}} \pi_{\xi} \cdot E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{\xi,v} \cdot \mathrm{W}^{e}_{v} + \sum_{\xi \in \Xi,\ k \in \mathcal{K}} \pi_{\xi} \cdot S_{k} \cdot \mathrm{c}^{\mathrm{cap},s}_{\xi,k} \cdot \mathrm{W}^{s}_{k} + \sum_{\xi \in \Xi,\ j \in \mathcal{J}} \pi_{\xi} \cdot Z_{j} \cdot \mathrm{c}^{\mathrm{cap},z}_{\xi,j} \cdot \mathrm{W}^{z}_{j} + \sum_{\xi \in \Xi,\ m \in \mathcal{M}} \pi_{\xi} \cdot \Sigma_{m} \cdot \mathrm{c}^{\mathrm{cap},\sigma}_{\xi,m} \cdot \mathrm{W}^{\sigma}_{m} + \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR ``` ### `Generator-fix-p-lower` @@ -3890,7 +4035,7 @@ Generator_fix_p_lower: ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( 1 - \gamma_{g} \cdot \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{\xi,t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{\xi,g} \cdot \left( 1 - \gamma_{\xi,g} \cdot \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-fix-p-upper` @@ -3906,7 +4051,7 @@ Generator_fix_p_upper: ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( 1 - \gamma_{g} \cdot \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{\xi,t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{\xi,g} \cdot \left( 1 - \gamma_{\xi,g} \cdot \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Link-fix-p-lower` @@ -3922,7 +4067,7 @@ Link_fix_p_lower: ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( 1 - \gamma^{f}_{l} \cdot \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{\xi,t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{\xi,l} \cdot \left( 1 - \gamma^{f}_{\xi,l} \cdot \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-fix-p-upper` @@ -3938,7 +4083,7 @@ Link_fix_p_upper: ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( 1 - \gamma^{f}_{l} \cdot \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{\xi,t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{\xi,l} \cdot \left( 1 - \gamma^{f}_{\xi,l} \cdot \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Generator-ext-p-lower` @@ -3954,7 +4099,7 @@ Generator_ext_p_lower: ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{\xi,t,g} \cdot \left( P_{g} - \gamma_{\xi,g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-ext-p-upper` @@ -3970,7 +4115,7 @@ Generator_ext_p_upper: ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{\xi,t,g} \cdot \left( P_{g} - \gamma_{\xi,g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \neg \mathrm{com}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-ext-p_nom-lower` @@ -3979,14 +4124,14 @@ p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdo ```yaml Generator_ext_p_nom_lower: - description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [generator] + description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, generator] where: Generator_p_nom_extendable expression: Generator_p_nom_ext >= Generator_p_nom_min ``` ```math -P_{g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} +P_{g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} ``` ### `Generator-ext-p_nom-upper` @@ -3995,14 +4140,14 @@ P_{g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \math ```yaml Generator_ext_p_nom_upper: - description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [generator] + description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, generator] where: Generator_p_nom_extendable AND Generator_p_nom_max expression: Generator_p_nom_ext <= Generator_p_nom_max ``` ```math -P_{g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \text{ is defined} +P_{g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \text{ is defined} ``` ### `Generator-p_nom_set` @@ -4012,13 +4157,13 @@ P_{g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathc ```yaml Generator_p_nom_set: description: "`Generator-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [generator] + dims: [scenario, generator] where: Generator_p_nom_extendable AND Generator_p_nom_set expression: Generator_p_nom_ext == Generator_p_nom_set ``` ```math -P_{g} = \mathrm{p}^{\mathrm{nom,set}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{nom,set}}_{g} \text{ is defined} +P_{g} = \mathrm{p}^{\mathrm{nom,set}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{nom,set}}_{\xi,g} \text{ is defined} ``` ### `Generator-e_sum_min` @@ -4034,7 +4179,7 @@ Generator_e_sum_min: ``` ```math -\sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \ge \underline{\mathrm{E}}_{g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \underline{\mathrm{E}}_{g} \text{ is defined} +\sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \ge \underline{\mathrm{E}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \underline{\mathrm{E}}_{\xi,g} \text{ is defined} ``` ### `Generator-e_sum_max` @@ -4050,7 +4195,7 @@ Generator_e_sum_max: ``` ```math -\sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \le \overline{\mathrm{E}}_{g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \overline{\mathrm{E}}_{g} \text{ is defined} +\sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathrm{w}^{\mathrm{gen}}_{t} \le \overline{\mathrm{E}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \overline{\mathrm{E}}_{\xi,g} \text{ is defined} ``` ### `Link-ext-p-lower` @@ -4066,7 +4211,7 @@ Link_ext_p_lower: ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{\xi,t,l} \cdot \left( F_{l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p-upper` @@ -4082,7 +4227,7 @@ Link_ext_p_upper: ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{\xi,t,l} \cdot \left( F_{l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \neg \mathrm{com}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-ext-p_nom-lower` @@ -4091,14 +4236,14 @@ f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} ```yaml Link_ext_p_nom_lower: - description: "`Link-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [link] + description: "`Link-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, link] where: Link_p_nom_extendable expression: Link_p_nom_ext >= Link_p_nom_min ``` ```math -F_{l} \ge \underline{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} +F_{l} \ge \underline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} ``` ### `Link-ext-p_nom-upper` @@ -4107,14 +4252,14 @@ F_{l} \ge \underline{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, l \in \math ```yaml Link_ext_p_nom_upper: - description: "`Link-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [link] + description: "`Link-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, link] where: Link_p_nom_extendable AND Link_p_nom_max expression: Link_p_nom_ext <= Link_p_nom_max ``` ```math -F_{l} \le \overline{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \overline{\mathrm{f}}^{\mathrm{nom}}_{l} \text{ is defined} +F_{l} \le \overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \text{ is defined} ``` ### `Link-p_nom_set` @@ -4124,13 +4269,13 @@ F_{l} \le \overline{\mathrm{f}}^{\mathrm{nom}}_{l} \qquad \forall\, l \in \mathc ```yaml Link_p_nom_set: description: "`Link-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [link] + dims: [scenario, link] where: Link_p_nom_extendable AND Link_p_nom_set expression: Link_p_nom_ext == Link_p_nom_set ``` ```math -F_{l} = \mathrm{f}^{\mathrm{nom,set}}_{l} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{nom,set}}_{l} \text{ is defined} +F_{l} = \mathrm{f}^{\mathrm{nom,set}}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{nom,set}}_{\xi,l} \text{ is defined} ``` ### `Process-fix-p-lower` @@ -4146,7 +4291,7 @@ Process_fix_p_lower: ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( 1 - \gamma^{z}_{j} \cdot \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{\xi,t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{\xi,j} \cdot \left( 1 - \gamma^{z}_{\xi,j} \cdot \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-fix-p-upper` @@ -4162,7 +4307,7 @@ Process_fix_p_upper: ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( 1 - \gamma^{z}_{j} \cdot \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{\xi,t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{\xi,j} \cdot \left( 1 - \gamma^{z}_{\xi,j} \cdot \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p-lower` @@ -4178,7 +4323,7 @@ Process_ext_p_lower: ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{\xi,t,j} \cdot \left( Z_{j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p-upper` @@ -4194,7 +4339,7 @@ Process_ext_p_upper: ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{\xi,t,j} \cdot \left( Z_{j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \neg \mathrm{com}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-ext-p_nom-lower` @@ -4203,14 +4348,14 @@ z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} ```yaml Process_ext_p_nom_lower: - description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [process] + description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, process] where: Process_p_nom_extendable expression: Process_p_nom_ext >= Process_p_nom_min ``` ```math -Z_{j} \ge \underline{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} +Z_{j} \ge \underline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} ``` ### `Process-ext-p_nom-upper` @@ -4219,14 +4364,14 @@ Z_{j} \ge \underline{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, j \in \math ```yaml Process_ext_p_nom_upper: - description: "`Process-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [process] + description: "`Process-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, process] where: Process_p_nom_extendable AND Process_p_nom_max expression: Process_p_nom_ext <= Process_p_nom_max ``` ```math -Z_{j} \le \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \text{ is defined} +Z_{j} \le \overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \text{ is defined} ``` ### `Process-p_nom_set` @@ -4236,13 +4381,13 @@ Z_{j} \le \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \qquad \forall\, j \in \mathc ```yaml Process_p_nom_set: description: "`Process-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [process] + dims: [scenario, process] where: Process_p_nom_extendable AND Process_p_nom_set expression: Process_p_nom_ext == Process_p_nom_set ``` ```math -Z_{j} = \mathrm{z}^{\mathrm{nom,set}}_{j} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{nom,set}}_{j} \text{ is defined} +Z_{j} = \mathrm{z}^{\mathrm{nom,set}}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{nom,set}}_{\xi,j} \text{ is defined} ``` ### `StorageUnit-fix-p_dispatch-lower` @@ -4274,7 +4419,7 @@ StorageUnit_fix_p_dispatch_upper: ``` ```math -h^{+}_{\xi,t,s} \le \overline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} +h^{+}_{\xi,t,s} \le \overline{\mathrm{h}}_{\xi,t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{\xi,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-fix-p_store-lower` @@ -4308,7 +4453,7 @@ StorageUnit_fix_p_store_upper: ``` ```math -h^{-}_{\xi,t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} +h^{-}_{\xi,t,s} \le -\underline{\mathrm{h}}_{\xi,t,s} \cdot \mathrm{h}^{\mathrm{nom}}_{\xi,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-fix-state_of_charge-lower` @@ -4340,7 +4485,7 @@ StorageUnit_fix_state_of_charge_upper: ``` ```math -\mathit{soc}_{\xi,t,s} \le \mathrm{T}^{h}_{s} \cdot \mathrm{h}^{\mathrm{nom}}_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} +\mathit{soc}_{\xi,t,s} \le \mathrm{T}^{h}_{\xi,s} \cdot \mathrm{h}^{\mathrm{nom}}_{\xi,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \neg \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `Generator-com-p-lower` @@ -4356,7 +4501,7 @@ Generator_com_p_lower: ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{\xi,t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{\xi,g} \cdot \left( u_{\xi,t,g} - \gamma_{\xi,g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-p-upper` @@ -4372,7 +4517,7 @@ Generator_com_p_upper: ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{\xi,t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{\xi,g} \cdot \left( u_{\xi,t,g} - \gamma_{\xi,g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-transition-start-up` @@ -4423,7 +4568,7 @@ Generator_com_up_time: ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}} \mathit{up}_{\xi,t',g} \le u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{UT}_{g} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}_{t,g} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}} \mathit{up}_{\xi,t',g} \le u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{UT}_{\xi,g} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-down-time` @@ -4442,7 +4587,7 @@ Generator_com_down_time: ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}} \mathit{dn}_{\xi,t',g} \le 1 - u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{DT}_{g} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}_{t,g} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}} \mathit{dn}_{\xi,t',g} \le 1 - u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{DT}_{\xi,g} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-status-min_up_time_must_stay_up` @@ -4458,7 +4603,7 @@ Generator_com_status_must_stay_up: ``` ```math -u_{\xi,t,g} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{hold}_{t,g} \wedge \mathrm{on}_{t,g} +u_{\xi,t,g} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{hold}_{\xi,t,g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-status-min_down_time_must_stay_up` @@ -4476,7 +4621,7 @@ Generator_com_status_must_stay_down: ``` ```math -u_{\xi,t,g} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{rest}_{t,g} \wedge \mathrm{on}_{t,g} +u_{\xi,t,g} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{rest}_{\xi,t,g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-run-bigM` @@ -4502,7 +4647,7 @@ Generator_p_ramp_limit_up_run_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-start-bigM` @@ -4528,7 +4673,7 @@ Generator_p_ramp_limit_up_start_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-run-bigM` @@ -4554,7 +4699,7 @@ Generator_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-shut-bigM` @@ -4580,7 +4725,7 @@ Generator_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot P_{g} + \mathrm{M}_{g} - \mathrm{M}_{g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p_nom_modularity` @@ -4614,7 +4759,7 @@ Generator_com_ext_p_upper_cap: ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{\xi,t,g} \cdot \left( P_{g} - \gamma_{\xi,g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-upper-bigM` @@ -4630,7 +4775,7 @@ Generator_com_ext_p_upper_big_m: ``` ```math -p_{\xi,t,g} \le \mathrm{M}_{g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \mathrm{M}_{\xi,g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-lower` @@ -4651,7 +4796,7 @@ Generator_com_ext_p_lower: ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \left( P_{g} - \gamma_{g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) + \mathrm{M}_{g} \cdot u_{\xi,t,g} - \mathrm{M}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{\xi,t,g} \cdot \left( P_{g} - \gamma_{\xi,g} \cdot \mu^{\mathrm{nom}}_{\xi,t,g} \right) + \mathrm{M}_{\xi,g} \cdot u_{\xi,t,g} - \mathrm{M}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-ext-p-lower-nonneg` @@ -4690,7 +4835,7 @@ Generator_com_mod_p_lower: ``` ```math -p_{\xi,t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \ge \underline{\mathrm{p}}_{\xi,t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{\xi,g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-com-mod-p-upper` @@ -4708,7 +4853,7 @@ Generator_com_mod_p_upper: ``` ```math -p_{\xi,t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} \le \overline{\mathrm{p}}_{\xi,t,g} \cdot \mathrm{p}^{\mathrm{mod}}_{g} \cdot \left( u_{\xi,t,g} - \gamma_{\xi,g} \cdot \mu^{u}_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-status-p-fixed-upper` @@ -4727,7 +4872,7 @@ Generator_status_p_fixed_upper: ``` ```math -u_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +u_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-start_up-p-fixed-upper` @@ -4746,7 +4891,7 @@ Generator_start_up_p_fixed_upper: ``` ```math -\mathit{up}_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +\mathit{up}_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-shut_down-p-fixed-upper` @@ -4765,7 +4910,7 @@ Generator_shut_down_p_fixed_upper: ``` ```math -\mathit{dn}_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +\mathit{dn}_{\xi,t,g} \le \mathrm{N}^{\mathrm{fix}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \neg \left( \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-status-p_nom-variable-upper` @@ -4829,7 +4974,7 @@ Generator_maint_event_count: ``` ```math -\sum_{t \in \mathcal{T}} \mu^{\mathrm{up}}_{\xi,t,g} = \mathrm{n}^{\mathrm{mnt}}_{g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +\sum_{t \in \mathcal{T}} \mu^{\mathrm{up}}_{\xi,t,g} = \mathrm{n}^{\mathrm{mnt}}_{\xi,g} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} ``` ### `Generator-maint-window` @@ -4848,7 +4993,7 @@ Generator_maint_window: ``` ```math -\mu_{\xi,t,g} = \sum_{t' \in \mathcal{T} \,:\, \left( g,\ t',\ t \right) \in \mathrm{Generator\_maintenance\_cover}} \mu^{\mathrm{up}}_{\xi,t',g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} +\mu_{\xi,t,g} = \sum_{t' \in \mathcal{T} \,:\, \left( \xi,\ g,\ t',\ t \right) \in \mathrm{Generator\_maintenance\_cover}} \mu^{\mathrm{up}}_{\xi,t',g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} ``` ### `Generator-maint-start-horizon` @@ -4864,7 +5009,7 @@ Generator_maint_start_horizon: ``` ```math -\mu^{\mathrm{up}}_{\xi,t,g} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} \wedge \mathrm{blk}_{t,g} +\mu^{\mathrm{up}}_{\xi,t,g} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{on}_{t,g} \wedge \mathrm{blk}_{\xi,t,g} ``` ### `Generator-maintcap_upper` @@ -4882,7 +5027,7 @@ Generator_maintcap_upper: ``` ```math -\mu^{\mathrm{nom}}_{\xi,t,g} \le P_{g} - \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +\mu^{\mathrm{nom}}_{\xi,t,g} \le P_{g} - \underline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-maintcap_upper_nommax` @@ -4898,7 +5043,7 @@ Generator_maintcap_upper_nommax: ``` ```math -\mu^{\mathrm{nom}}_{\xi,t,g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +\mu^{\mathrm{nom}}_{\xi,t,g} \le \overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-maintcap_lower_nommax` @@ -4914,7 +5059,7 @@ Generator_maintcap_lower_nommax: ``` ```math -\mu^{\mathrm{nom}}_{\xi,t,g} \ge P_{g} - \overline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} +\mu^{\mathrm{nom}}_{\xi,t,g} \ge P_{g} - \overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-maintcap_lower_nommin` @@ -4930,7 +5075,7 @@ Generator_maintcap_lower_nommin: ``` ```math -\mu^{\mathrm{nom}}_{\xi,t,g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} \wedge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} > 0 +\mu^{\mathrm{nom}}_{\xi,t,g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \mathrm{on}_{t,g} \wedge \underline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} > 0 ``` ### `Generator-maint-status-le-status` @@ -5012,7 +5157,7 @@ Generator_maint_modstatus_le_maint: ``` ```math -\mu^{u}_{\xi,t,g} \le \frac{\overline{\mathrm{p}}^{\mathrm{nom}}_{g}}{\mathrm{p}^{\mathrm{mod}}_{g}} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +\mu^{u}_{\xi,t,g} \le \frac{\overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g}}{\mathrm{p}^{\mathrm{mod}}_{g}} \cdot \mu_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Generator-maint-modstatus-lb` @@ -5028,7 +5173,7 @@ Generator_maint_modstatus_lb: ``` ```math -\mu^{u}_{\xi,t,g} \ge u_{\xi,t,g} - \frac{\overline{\mathrm{p}}^{\mathrm{nom}}_{g}}{\mathrm{p}^{\mathrm{mod}}_{g}} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} +\mu^{u}_{\xi,t,g} \ge u_{\xi,t,g} - \frac{\overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g}}{\mathrm{p}^{\mathrm{mod}}_{g}} \cdot \left( 1 - \mu_{\xi,t,g} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \wedge \mathrm{on}_{t,g} ``` ### `Link-com-p-lower` @@ -5044,7 +5189,7 @@ Link_com_p_lower: ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{\xi,t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{\xi,l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-p-upper` @@ -5060,7 +5205,7 @@ Link_com_p_upper: ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{\xi,t,l} \cdot \mathrm{f}^{\mathrm{nom}}_{\xi,l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-transition-start-up` @@ -5111,7 +5256,7 @@ Link_com_up_time: ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}^{f}} \mathit{up}^{f}_{\xi,t',l} \le u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{UT}^{f}_{l} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{f}_{t,l} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}^{f}} \mathit{up}^{f}_{\xi,t',l} \le u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{UT}^{f}_{\xi,l} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-down-time` @@ -5130,7 +5275,7 @@ Link_com_down_time: ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}^{f}} \mathit{dn}^{f}_{\xi,t',l} \le 1 - u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{DT}^{f}_{l} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{f}_{t,l} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}^{f}} \mathit{dn}^{f}_{\xi,t',l} \le 1 - u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{DT}^{f}_{\xi,l} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-status-min_up_time_must_stay_up` @@ -5146,7 +5291,7 @@ Link_com_status_must_stay_up: ``` ```math -u^{f}_{\xi,t,l} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{hold}^{f}_{t,l} \wedge \mathrm{on}^{f}_{t,l} +u^{f}_{\xi,t,l} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{hold}^{f}_{\xi,t,l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-status-min_down_time_must_stay_up` @@ -5164,7 +5309,7 @@ Link_com_status_must_stay_down: ``` ```math -u^{f}_{\xi,t,l} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{rest}^{f}_{t,l} \wedge \mathrm{on}^{f}_{t,l} +u^{f}_{\xi,t,l} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{rest}^{f}_{\xi,t,l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_up-run-bigM` @@ -5190,7 +5335,7 @@ Link_p_ramp_limit_up_run_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_up-start-bigM` @@ -5216,7 +5361,7 @@ Link_p_ramp_limit_up_start_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-run-bigM` @@ -5242,7 +5387,7 @@ Link_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-shut-bigM` @@ -5268,7 +5413,7 @@ Link_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} \cdot F_{l} + \mathrm{M}^{f}_{l} - \mathrm{M}^{f}_{l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p_nom_modularity` @@ -5302,7 +5447,7 @@ Link_com_ext_p_upper_cap: ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{\xi,t,l} \cdot \left( F_{l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-ext-p-upper-bigM` @@ -5318,7 +5463,7 @@ Link_com_ext_p_upper_big_m: ``` ```math -f_{\xi,t,l} \le \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \mathrm{M}^{f}_{\xi,l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-ext-p-lower` @@ -5339,7 +5484,7 @@ Link_com_ext_p_lower: ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \left( F_{l} - \gamma^{f}_{l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) + \mathrm{M}^{f}_{l} \cdot u^{f}_{\xi,t,l} - \mathrm{M}^{f}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{\xi,t,l} \cdot \left( F_{l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,\mathrm{nom}}_{\xi,t,l} \right) + \mathrm{M}^{f}_{\xi,l} \cdot u^{f}_{\xi,t,l} - \mathrm{M}^{f}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-ext-p-lower-nonneg` @@ -5378,7 +5523,7 @@ Link_com_mod_p_lower: ``` ```math -f_{\xi,t,l} \ge \underline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \ge \underline{\mathrm{f}}_{\xi,t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-com-mod-p-upper` @@ -5396,7 +5541,7 @@ Link_com_mod_p_upper: ``` ```math -f_{\xi,t,l} \le \overline{\mathrm{f}}_{t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} \le \overline{\mathrm{f}}_{\xi,t,l} \cdot \mathrm{f}^{\mathrm{mod}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \gamma^{f}_{\xi,l} \cdot \mu^{f,u}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-status-p-fixed-upper` @@ -5415,7 +5560,7 @@ Link_status_p_fixed_upper: ``` ```math -u^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +u^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-start_up-p-fixed-upper` @@ -5434,7 +5579,7 @@ Link_start_up_p_fixed_upper: ``` ```math -\mathit{up}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mathit{up}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-shut_down-p-fixed-upper` @@ -5453,7 +5598,7 @@ Link_shut_down_p_fixed_upper: ``` ```math -\mathit{dn}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mathit{dn}^{f}_{\xi,t,l} \le \mathrm{N}^{f,\mathrm{fix}}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \neg \left( \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-status-p_nom-variable-upper` @@ -5517,7 +5662,7 @@ Link_maint_event_count: ``` ```math -\sum_{t \in \mathcal{T}} \mu^{f,\mathrm{up}}_{\xi,t,l} = \mathrm{n}^{f,\mathrm{mnt}}_{l} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +\sum_{t \in \mathcal{T}} \mu^{f,\mathrm{up}}_{\xi,t,l} = \mathrm{n}^{f,\mathrm{mnt}}_{\xi,l} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} ``` ### `Link-maint-window` @@ -5536,7 +5681,7 @@ Link_maint_window: ``` ```math -\mu^{f}_{\xi,t,l} = \sum_{t' \in \mathcal{T} \,:\, \left( l,\ t',\ t \right) \in \mathrm{Link\_maintenance\_cover}} \mu^{f,\mathrm{up}}_{\xi,t',l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} +\mu^{f}_{\xi,t,l} = \sum_{t' \in \mathcal{T} \,:\, \left( \xi,\ l,\ t',\ t \right) \in \mathrm{Link\_maintenance\_cover}} \mu^{f,\mathrm{up}}_{\xi,t',l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-maint-start-horizon` @@ -5552,7 +5697,7 @@ Link_maint_start_horizon: ``` ```math -\mu^{f,\mathrm{up}}_{\xi,t,l} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} \wedge \mathrm{blk}^{f}_{t,l} +\mu^{f,\mathrm{up}}_{\xi,t,l} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{on}^{f}_{t,l} \wedge \mathrm{blk}^{f}_{\xi,t,l} ``` ### `Link-maintcap_upper` @@ -5570,7 +5715,7 @@ Link_maintcap_upper: ``` ```math -\mu^{f,\mathrm{nom}}_{\xi,t,l} \le F_{l} - \underline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,\mathrm{nom}}_{\xi,t,l} \le F_{l} - \underline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-maintcap_upper_nommax` @@ -5586,7 +5731,7 @@ Link_maintcap_upper_nommax: ``` ```math -\mu^{f,\mathrm{nom}}_{\xi,t,l} \le \overline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,\mathrm{nom}}_{\xi,t,l} \le \overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-maintcap_lower_nommax` @@ -5602,7 +5747,7 @@ Link_maintcap_lower_nommax: ``` ```math -\mu^{f,\mathrm{nom}}_{\xi,t,l} \ge F_{l} - \overline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,\mathrm{nom}}_{\xi,t,l} \ge F_{l} - \overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-maintcap_lower_nommin` @@ -5618,7 +5763,7 @@ Link_maintcap_lower_nommin: ``` ```math -\mu^{f,\mathrm{nom}}_{\xi,t,l} \ge \underline{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} \wedge \underline{\mathrm{f}}^{\mathrm{nom}}_{l} > 0 +\mu^{f,\mathrm{nom}}_{\xi,t,l} \ge \underline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \mathrm{on}^{f}_{t,l} \wedge \underline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} > 0 ``` ### `Link-maint-status-le-status` @@ -5700,7 +5845,7 @@ Link_maint_modstatus_le_maint: ``` ```math -\mu^{f,u}_{\xi,t,l} \le \frac{\overline{\mathrm{f}}^{\mathrm{nom}}_{l}}{\mathrm{f}^{\mathrm{mod}}_{l}} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \le \frac{\overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l}}{\mathrm{f}^{\mathrm{mod}}_{l}} \cdot \mu^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-maint-modstatus-lb` @@ -5716,7 +5861,7 @@ Link_maint_modstatus_lb: ``` ```math -\mu^{f,u}_{\xi,t,l} \ge u^{f}_{\xi,t,l} - \frac{\overline{\mathrm{f}}^{\mathrm{nom}}_{l}}{\mathrm{f}^{\mathrm{mod}}_{l}} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} +\mu^{f,u}_{\xi,t,l} \ge u^{f}_{\xi,t,l} - \frac{\overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l}}{\mathrm{f}^{\mathrm{mod}}_{l}} \cdot \left( 1 - \mu^{f}_{\xi,t,l} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-com-p-lower` @@ -5732,7 +5877,7 @@ Process_com_p_lower: ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{\xi,t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{\xi,j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-p-upper` @@ -5748,7 +5893,7 @@ Process_com_p_upper: ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{\xi,t,j} \cdot \mathrm{z}^{\mathrm{nom}}_{\xi,j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-transition-start-up` @@ -5799,7 +5944,7 @@ Process_com_up_time: ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}^{z}} \mathit{up}^{z}_{\xi,t',j} \le u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{UT}^{z}_{j} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{z}_{t,j} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{UT}^{z}} \mathit{up}^{z}_{\xi,t',j} \le u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{UT}^{z}_{\xi,j} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-down-time` @@ -5818,7 +5963,7 @@ Process_com_down_time: ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}^{z}} \mathit{dn}^{z}_{\xi,t',j} \le 1 - u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{DT}^{z}_{j} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{z}_{t,j} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < \mathrm{DT}^{z}} \mathit{dn}^{z}_{\xi,t',j} \le 1 - u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{DT}^{z}_{\xi,j} > 0 \wedge \mathrm{pos}(t) > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-status-min_up_time_must_stay_up` @@ -5834,7 +5979,7 @@ Process_com_status_must_stay_up: ``` ```math -u^{z}_{\xi,t,j} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{hold}^{z}_{t,j} \wedge \mathrm{on}^{z}_{t,j} +u^{z}_{\xi,t,j} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{hold}^{z}_{\xi,t,j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-status-min_down_time_must_stay_up` @@ -5852,7 +5997,7 @@ Process_com_status_must_stay_down: ``` ```math -u^{z}_{\xi,t,j} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{rest}^{z}_{t,j} \wedge \mathrm{on}^{z}_{t,j} +u^{z}_{\xi,t,j} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{rest}^{z}_{\xi,t,j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_up-run-bigM` @@ -5878,7 +6023,7 @@ Process_p_ramp_limit_up_run_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_up-start-bigM` @@ -5904,7 +6049,7 @@ Process_p_ramp_limit_up_start_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-run-bigM` @@ -5930,7 +6075,7 @@ Process_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-shut-bigM` @@ -5956,7 +6101,7 @@ Process_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} \cdot Z_{j} + \mathrm{M}^{z}_{j} - \mathrm{M}^{z}_{j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p_nom_modularity` @@ -5990,7 +6135,7 @@ Process_com_ext_p_upper_cap: ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{\xi,t,j} \cdot \left( Z_{j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-ext-p-upper-bigM` @@ -6006,7 +6151,7 @@ Process_com_ext_p_upper_big_m: ``` ```math -z_{\xi,t,j} \le \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \mathrm{M}^{z}_{\xi,j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-ext-p-lower` @@ -6027,7 +6172,7 @@ Process_com_ext_p_lower: ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \left( Z_{j} - \gamma^{z}_{j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) + \mathrm{M}^{z}_{j} \cdot u^{z}_{\xi,t,j} - \mathrm{M}^{z}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{\xi,t,j} \cdot \left( Z_{j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,\mathrm{nom}}_{\xi,t,j} \right) + \mathrm{M}^{z}_{\xi,j} \cdot u^{z}_{\xi,t,j} - \mathrm{M}^{z}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-ext-p-lower-nonneg` @@ -6066,7 +6211,7 @@ Process_com_mod_p_lower: ``` ```math -z_{\xi,t,j} \ge \underline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \ge \underline{\mathrm{z}}_{\xi,t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-com-mod-p-upper` @@ -6084,7 +6229,7 @@ Process_com_mod_p_upper: ``` ```math -z_{\xi,t,j} \le \overline{\mathrm{z}}_{t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} \le \overline{\mathrm{z}}_{\xi,t,j} \cdot \mathrm{z}^{\mathrm{mod}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \gamma^{z}_{\xi,j} \cdot \mu^{z,u}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-status-p-fixed-upper` @@ -6103,7 +6248,7 @@ Process_status_p_fixed_upper: ``` ```math -u^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +u^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-start_up-p-fixed-upper` @@ -6122,7 +6267,7 @@ Process_start_up_p_fixed_upper: ``` ```math -\mathit{up}^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\mathit{up}^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-shut_down-p-fixed-upper` @@ -6141,7 +6286,7 @@ Process_shut_down_p_fixed_upper: ``` ```math -\mathit{dn}^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\mathit{dn}^{z}_{\xi,t,j} \le \mathrm{N}^{z,\mathrm{fix}}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \neg \left( \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-status-p_nom-variable-upper` @@ -6205,7 +6350,7 @@ Process_maint_event_count: ``` ```math -\sum_{t \in \mathcal{T}} \mu^{z,\mathrm{up}}_{\xi,t,j} = \mathrm{n}^{z,\mathrm{mnt}}_{j} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +\sum_{t \in \mathcal{T}} \mu^{z,\mathrm{up}}_{\xi,t,j} = \mathrm{n}^{z,\mathrm{mnt}}_{\xi,j} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} ``` ### `Process-maint-window` @@ -6224,7 +6369,7 @@ Process_maint_window: ``` ```math -\mu^{z}_{\xi,t,j} = \sum_{t' \in \mathcal{T} \,:\, \left( j,\ t',\ t \right) \in \mathrm{Process\_maintenance\_cover}} \mu^{z,\mathrm{up}}_{\xi,t',j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} +\mu^{z}_{\xi,t,j} = \sum_{t' \in \mathcal{T} \,:\, \left( \xi,\ j,\ t',\ t \right) \in \mathrm{Process\_maintenance\_cover}} \mu^{z,\mathrm{up}}_{\xi,t',j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-maint-start-horizon` @@ -6240,7 +6385,7 @@ Process_maint_start_horizon: ``` ```math -\mu^{z,\mathrm{up}}_{\xi,t,j} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} \wedge \mathrm{blk}^{z}_{t,j} +\mu^{z,\mathrm{up}}_{\xi,t,j} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{on}^{z}_{t,j} \wedge \mathrm{blk}^{z}_{\xi,t,j} ``` ### `Process-maintcap_upper` @@ -6258,7 +6403,7 @@ Process_maintcap_upper: ``` ```math -\mu^{z,\mathrm{nom}}_{\xi,t,j} \le Z_{j} - \underline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\mu^{z,\mathrm{nom}}_{\xi,t,j} \le Z_{j} - \underline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-maintcap_upper_nommax` @@ -6274,7 +6419,7 @@ Process_maintcap_upper_nommax: ``` ```math -\mu^{z,\mathrm{nom}}_{\xi,t,j} \le \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\mu^{z,\mathrm{nom}}_{\xi,t,j} \le \overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-maintcap_lower_nommax` @@ -6290,7 +6435,7 @@ Process_maintcap_lower_nommax: ``` ```math -\mu^{z,\mathrm{nom}}_{\xi,t,j} \ge Z_{j} - \overline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\mu^{z,\mathrm{nom}}_{\xi,t,j} \ge Z_{j} - \overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-maintcap_lower_nommin` @@ -6306,7 +6451,7 @@ Process_maintcap_lower_nommin: ``` ```math -\mu^{z,\mathrm{nom}}_{\xi,t,j} \ge \underline{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} \wedge \underline{\mathrm{z}}^{\mathrm{nom}}_{j} > 0 +\mu^{z,\mathrm{nom}}_{\xi,t,j} \ge \underline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \mathrm{on}^{z}_{t,j} \wedge \underline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} > 0 ``` ### `Process-maint-status-le-status` @@ -6388,7 +6533,7 @@ Process_maint_modstatus_le_maint: ``` ```math -\mu^{z,u}_{\xi,t,j} \le \frac{\overline{\mathrm{z}}^{\mathrm{nom}}_{j}}{\mathrm{z}^{\mathrm{mod}}_{j}} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +\mu^{z,u}_{\xi,t,j} \le \frac{\overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j}}{\mathrm{z}^{\mathrm{mod}}_{j}} \cdot \mu^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-maint-modstatus-lb` @@ -6404,7 +6549,7 @@ Process_maint_modstatus_lb: ``` ```math -\mu^{z,u}_{\xi,t,j} \ge u^{z}_{\xi,t,j} - \frac{\overline{\mathrm{z}}^{\mathrm{nom}}_{j}}{\mathrm{z}^{\mathrm{mod}}_{j}} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} +\mu^{z,u}_{\xi,t,j} \ge u^{z}_{\xi,t,j} - \frac{\overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j}}{\mathrm{z}^{\mathrm{mod}}_{j}} \cdot \left( 1 - \mu^{z}_{\xi,t,j} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \wedge \mathrm{on}^{z}_{t,j} ``` ### `Line-fix-s-lower` @@ -6420,7 +6565,7 @@ Line_fix_s_lower: ``` ```math -s_{\xi,t,k} - \ell_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} - \ell_{\xi,t,k} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-fix-s-upper` @@ -6436,7 +6581,7 @@ Line_fix_s_upper: ``` ```math -s_{\xi,t,k} + \ell_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} + \ell_{\xi,t,k} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s-lower` @@ -6452,7 +6597,7 @@ Line_ext_s_lower: ``` ```math -s_{\xi,t,k} - \ell_{\xi,t,k} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} - \ell_{\xi,t,k} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s-upper` @@ -6468,7 +6613,7 @@ Line_ext_s_upper: ``` ```math -s_{\xi,t,k} + \ell_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} + \ell_{\xi,t,k} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-ext-s_nom-lower` @@ -6477,14 +6622,14 @@ s_{\xi,t,k} + \ell_{\xi,t,k} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad ```yaml Line_ext_s_nom_lower: - description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor" - dims: [line] + description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, line] where: Line_s_nom_extendable expression: Line_s_nom_ext >= Line_s_nom_min ``` ```math -S_{k} \ge \underline{\mathrm{s}}^{\mathrm{nom}}_{k} \qquad \forall\, k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} +S_{k} \ge \underline{\mathrm{s}}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} ``` ### `Line-ext-s_nom-upper` @@ -6493,14 +6638,14 @@ S_{k} \ge \underline{\mathrm{s}}^{\mathrm{nom}}_{k} \qquad \forall\, k \in \math ```yaml Line_ext_s_nom_upper: - description: "`Line-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [line] + description: "`Line-ext-s_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, line] where: Line_s_nom_extendable AND Line_s_nom_max expression: Line_s_nom_ext <= Line_s_nom_max ``` ```math -S_{k} \le \overline{\mathrm{s}}^{\mathrm{nom}}_{k} \qquad \forall\, k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \overline{\mathrm{s}}^{\mathrm{nom}}_{k} \text{ is defined} +S_{k} \le \overline{\mathrm{s}}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \overline{\mathrm{s}}^{\mathrm{nom}}_{\xi,k} \text{ is defined} ``` ### `Line-s_nom_set` @@ -6510,13 +6655,13 @@ S_{k} \le \overline{\mathrm{s}}^{\mathrm{nom}}_{k} \qquad \forall\, k \in \mathc ```yaml Line_s_nom_set: description: "`Line-s_nom_set` — the chosen build pinned, wherever a value is given" - dims: [line] + dims: [scenario, line] where: Line_s_nom_extendable AND Line_s_nom_set expression: Line_s_nom_ext == Line_s_nom_set ``` ```math -S_{k} = \mathrm{s}^{\mathrm{nom,set}}_{k} \qquad \forall\, k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{s}^{\mathrm{nom,set}}_{k} \text{ is defined} +S_{k} = \mathrm{s}^{\mathrm{nom,set}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ k \in \mathcal{K} \,:\, \mathrm{ext}^{s}_{k} \wedge \mathrm{s}^{\mathrm{nom,set}}_{\xi,k} \text{ is defined} ``` ### `Line-s_set` @@ -6532,7 +6677,7 @@ Line_s_set: ``` ```math -s_{\xi,t,k} = \mathrm{s}^{\mathrm{set}}_{t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{s}^{\mathrm{set}}_{t,k} \text{ is defined} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} = \mathrm{s}^{\mathrm{set}}_{\xi,t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{s}^{\mathrm{set}}_{\xi,t,k} \text{ is defined} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-loss_upper` @@ -6548,7 +6693,7 @@ Line_loss_upper: ``` ```math -\ell_{\xi,t,k} \le \overline{\ell}_{t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} +\ell_{\xi,t,k} \le \overline{\ell}_{\xi,t,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-loss_tangents-{k}-1` @@ -6568,7 +6713,7 @@ Line_loss_tangents_forward: ``` ```math -\ell_{\xi,t,k} + \mathrm{a}_{t,k,b} \cdot s_{\xi,t,k} \ge \mathrm{b}_{t,k,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} +\ell_{\xi,t,k} + \mathrm{a}_{\xi,t,k,b} \cdot s_{\xi,t,k} \ge \mathrm{b}_{\xi,t,k,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-loss_tangents-{k}--1` @@ -6586,7 +6731,7 @@ Line_loss_tangents_reverse: ``` ```math -\ell_{\xi,t,k} - \mathrm{a}_{t,k,b} \cdot s_{\xi,t,k} \ge \mathrm{b}_{t,k,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} +\ell_{\xi,t,k} - \mathrm{a}_{\xi,t,k,b} \cdot s_{\xi,t,k} \ge \mathrm{b}_{\xi,t,k,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Transformer-fix-s-lower` @@ -6602,7 +6747,7 @@ Transformer_fix_s_lower: ``` ```math -\sigma_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-fix-s-upper` @@ -6618,7 +6763,7 @@ Transformer_fix_s_upper: ``` ```math -\sigma_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-ext-s-lower` @@ -6634,7 +6779,7 @@ Transformer_ext_s_lower: ``` ```math -\sigma_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} \ge -\overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-ext-s-upper` @@ -6650,7 +6795,7 @@ Transformer_ext_s_upper: ``` ```math -\sigma_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} \le \overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-ext-s_nom-lower` @@ -6659,14 +6804,14 @@ Transformer_ext_s_upper: ```yaml Transformer_ext_s_nom_lower: - description: "`Transformer-ext-s_nom-lower` — the chosen build is at least its floor" - dims: [transformer] + description: "`Transformer-ext-s_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, transformer] where: Transformer_s_nom_extendable expression: Transformer_s_nom_ext >= Transformer_s_nom_min ``` ```math -\Sigma_{m} \ge \underline{\sigma}^{\mathrm{nom}}_{m} \qquad \forall\, m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} +\Sigma_{m} \ge \underline{\sigma}^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} ``` ### `Transformer-ext-s_nom-upper` @@ -6675,14 +6820,14 @@ Transformer_ext_s_nom_lower: ```yaml Transformer_ext_s_nom_upper: - description: "`Transformer-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [transformer] + description: "`Transformer-ext-s_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, transformer] where: Transformer_s_nom_extendable AND Transformer_s_nom_max expression: Transformer_s_nom_ext <= Transformer_s_nom_max ``` ```math -\Sigma_{m} \le \overline{\sigma}^{\mathrm{nom}}_{m} \qquad \forall\, m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \overline{\sigma}^{\mathrm{nom}}_{m} \text{ is defined} +\Sigma_{m} \le \overline{\sigma}^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \overline{\sigma}^{\mathrm{nom}}_{\xi,m} \text{ is defined} ``` ### `Transformer-s_nom_set` @@ -6692,13 +6837,13 @@ Transformer_ext_s_nom_upper: ```yaml Transformer_s_nom_set: description: "`Transformer-s_nom_set` — the chosen build pinned, wherever a value is given" - dims: [transformer] + dims: [scenario, transformer] where: Transformer_s_nom_extendable AND Transformer_s_nom_set expression: Transformer_s_nom_ext == Transformer_s_nom_set ``` ```math -\Sigma_{m} = \sigma^{\mathrm{nom,set}}_{m} \qquad \forall\, m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \sigma^{\mathrm{nom,set}}_{m} \text{ is defined} +\Sigma_{m} = \sigma^{\mathrm{nom,set}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ m \in \mathcal{M} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \sigma^{\mathrm{nom,set}}_{\xi,m} \text{ is defined} ``` ### `Transformer-s_set` @@ -6714,7 +6859,7 @@ Transformer_s_set: ``` ```math -\sigma_{\xi,t,m} = \sigma^{\mathrm{set}}_{t,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \sigma^{\mathrm{set}}_{t,m} \text{ is defined} \wedge \mathrm{on}^{\sigma}_{t,m} +\sigma_{\xi,t,m} = \sigma^{\mathrm{set}}_{\xi,t,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \sigma^{\mathrm{set}}_{\xi,t,m} \text{ is defined} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-loss_upper` @@ -6730,7 +6875,7 @@ Transformer_loss_upper: ``` ```math -\ell^{\sigma}_{\xi,t,m} \le \overline{\ell}^{\sigma}_{t,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} +\ell^{\sigma}_{\xi,t,m} \le \overline{\ell}^{\sigma}_{\xi,t,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-loss_tangents-{k}-1` @@ -6749,7 +6894,7 @@ Transformer_loss_tangents_forward: ``` ```math -\ell^{\sigma}_{\xi,t,m} + \mathrm{a}^{\sigma}_{t,m,b} \cdot \sigma_{\xi,t,m} \ge \mathrm{b}^{\sigma}_{t,m,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} +\ell^{\sigma}_{\xi,t,m} + \mathrm{a}^{\sigma}_{\xi,t,m,b} \cdot \sigma_{\xi,t,m} \ge \mathrm{b}^{\sigma}_{\xi,t,m,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Transformer-loss_tangents-{k}--1` @@ -6767,7 +6912,7 @@ Transformer_loss_tangents_reverse: ``` ```math -\ell^{\sigma}_{\xi,t,m} - \mathrm{a}^{\sigma}_{t,m,b} \cdot \sigma_{\xi,t,m} \ge \mathrm{b}^{\sigma}_{t,m,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} +\ell^{\sigma}_{\xi,t,m} - \mathrm{a}^{\sigma}_{\xi,t,m,b} \cdot \sigma_{\xi,t,m} \ge \mathrm{b}^{\sigma}_{\xi,t,m,b} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ b \in \mathcal{B} \,:\, \mathrm{lossy} \wedge \mathrm{on}^{\sigma}_{t,m} ``` ### `Line-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -6789,7 +6934,7 @@ Line_fix_s_lower_security: ``` ```math -\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -6810,7 +6955,7 @@ Line_fix_s_upper_security: ``` ```math -\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -6832,7 +6977,7 @@ Line_ext_s_lower_security: ``` ```math -\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -6854,7 +6999,7 @@ Line_ext_s_upper_security: ``` ```math -\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -6876,7 +7021,7 @@ Transformer_fix_s_lower_security: ``` ```math -\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -6897,7 +7042,7 @@ Transformer_fix_s_upper_security: ``` ```math -\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{t,m} \cdot \sigma^{\mathrm{nom}}_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -6919,7 +7064,7 @@ Transformer_ext_s_lower_security: ``` ```math -\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -6941,7 +7086,7 @@ Transformer_ext_s_upper_security: ``` ```math -\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Kirchhoff-Voltage-Law` @@ -6991,7 +7136,7 @@ Generator_p_ramp_limit_up: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{\xi,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down` @@ -7016,7 +7161,7 @@ Generator_p_ramp_limit_down: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{\xi,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Link-p-ramp_limit_up` @@ -7041,7 +7186,7 @@ Link_p_ramp_limit_up: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down` @@ -7066,7 +7211,7 @@ Link_p_ramp_limit_down: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-p-ramp_limit_up` @@ -7091,7 +7236,7 @@ Process_p_ramp_limit_up: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down` @@ -7116,7 +7261,7 @@ Process_p_ramp_limit_down: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `StorageUnit-ext-p_dispatch-lower` @@ -7148,7 +7293,7 @@ StorageUnit_ext_p_dispatch_upper: ``` ```math -h^{+}_{\xi,t,s} \le \overline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} +h^{+}_{\xi,t,s} \le \overline{\mathrm{h}}_{\xi,t,s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-p_store-lower` @@ -7182,7 +7327,7 @@ StorageUnit_ext_p_store_upper: ``` ```math -h^{-}_{\xi,t,s} \le -\underline{\mathrm{h}}_{t,s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} +h^{-}_{\xi,t,s} \le -\underline{\mathrm{h}}_{\xi,t,s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-state_of_charge-lower` @@ -7214,7 +7359,7 @@ StorageUnit_ext_state_of_charge_upper: ``` ```math -\mathit{soc}_{\xi,t,s} \le \mathrm{T}^{h}_{s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} +\mathit{soc}_{\xi,t,s} \le \mathrm{T}^{h}_{\xi,s} \cdot H_{s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-ext-p_nom-lower` @@ -7223,14 +7368,14 @@ StorageUnit_ext_state_of_charge_upper: ```yaml StorageUnit_ext_p_nom_lower: - description: "`StorageUnit-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [storage_unit] + description: "`StorageUnit-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, storage_unit] where: StorageUnit_p_nom_extendable expression: StorageUnit_p_nom_ext >= StorageUnit_p_nom_min ``` ```math -H_{s} \ge \underline{\mathrm{h}}^{\mathrm{nom}}_{s} \qquad \forall\, s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} +H_{s} \ge \underline{\mathrm{h}}^{\mathrm{nom}}_{\xi,s} \qquad \forall\, \xi \in \Xi,\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} ``` ### `StorageUnit-ext-p_nom-upper` @@ -7239,14 +7384,14 @@ H_{s} \ge \underline{\mathrm{h}}^{\mathrm{nom}}_{s} \qquad \forall\, s \in \math ```yaml StorageUnit_ext_p_nom_upper: - description: "`StorageUnit-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [storage_unit] + description: "`StorageUnit-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, storage_unit] where: StorageUnit_p_nom_extendable AND StorageUnit_p_nom_max expression: StorageUnit_p_nom_ext <= StorageUnit_p_nom_max ``` ```math -H_{s} \le \overline{\mathrm{h}}^{\mathrm{nom}}_{s} \qquad \forall\, s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \overline{\mathrm{h}}^{\mathrm{nom}}_{s} \text{ is defined} +H_{s} \le \overline{\mathrm{h}}^{\mathrm{nom}}_{\xi,s} \qquad \forall\, \xi \in \Xi,\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \overline{\mathrm{h}}^{\mathrm{nom}}_{\xi,s} \text{ is defined} ``` ### `StorageUnit-p_nom_set` @@ -7256,13 +7401,13 @@ H_{s} \le \overline{\mathrm{h}}^{\mathrm{nom}}_{s} \qquad \forall\, s \in \mathc ```yaml StorageUnit_p_nom_set: description: "`StorageUnit-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [storage_unit] + dims: [scenario, storage_unit] where: StorageUnit_p_nom_extendable AND StorageUnit_p_nom_set expression: StorageUnit_p_nom_ext == StorageUnit_p_nom_set ``` ```math -H_{s} = \mathrm{h}^{\mathrm{nom,set}}_{s} \qquad \forall\, s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{h}^{\mathrm{nom,set}}_{s} \text{ is defined} +H_{s} = \mathrm{h}^{\mathrm{nom,set}}_{\xi,s} \qquad \forall\, \xi \in \Xi,\ s \in \mathcal{S} \,:\, \mathrm{ext}^{h}_{s} \wedge \mathrm{h}^{\mathrm{nom,set}}_{\xi,s} \text{ is defined} ``` ### `StorageUnit-energy_balance` @@ -7286,7 +7431,7 @@ StorageUnit_energy_balance: ``` ```math -\mathit{soc}_{\xi,t,s} = \overleftarrow{\mathit{soc}}_{\xi,t,s} + \eta^{-}_{s} \cdot h^{-}_{\xi,t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t} - \frac{h^{+}_{\xi,t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t}}{\eta^{+}_{s}} + \left( \mathrm{inflow}_{t,s} - \mathit{spill}_{\xi,t,s} \right) \cdot \mathrm{w}^{\mathrm{sto}}_{t} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{on}^{h}_{t,s} +\mathit{soc}_{\xi,t,s} = \overleftarrow{\mathit{soc}}_{\xi,t,s} + \eta^{-}_{\xi,s} \cdot h^{-}_{\xi,t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t} - \frac{h^{+}_{\xi,t,s} \cdot \mathrm{w}^{\mathrm{sto}}_{t}}{\eta^{+}_{\xi,s}} + \left( \mathrm{inflow}_{\xi,t,s} - \mathit{spill}_{\xi,t,s} \right) \cdot \mathrm{w}^{\mathrm{sto}}_{t} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{on}^{h}_{t,s} ``` ### `Store-fix-e-lower` @@ -7302,7 +7447,7 @@ Store_fix_e_lower: ``` ```math -e_{\xi,t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} +e_{\xi,t,v} \ge \underline{\mathrm{e}}_{\xi,t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-fix-e-upper` @@ -7318,7 +7463,7 @@ Store_fix_e_upper: ``` ```math -e_{\xi,t,v} \le \overline{\mathrm{e}}_{t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} +e_{\xi,t,v} \le \overline{\mathrm{e}}_{\xi,t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-ext-e-lower` @@ -7334,7 +7479,7 @@ Store_ext_e_lower: ``` ```math -e_{\xi,t,v} \ge \underline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} +e_{\xi,t,v} \ge \underline{\mathrm{e}}_{\xi,t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-ext-e-upper` @@ -7350,7 +7495,7 @@ Store_ext_e_upper: ``` ```math -e_{\xi,t,v} \le \overline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} +e_{\xi,t,v} \le \overline{\mathrm{e}}_{\xi,t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-ext-e_nom-lower` @@ -7359,14 +7504,14 @@ e_{\xi,t,v} \le \overline{\mathrm{e}}_{t,v} \cdot E_{v} \qquad \forall\, \xi \in ```yaml Store_ext_e_nom_lower: - description: "`Store-ext-e_nom-lower` — the chosen build is at least its floor" - dims: [store] + description: "`Store-ext-e_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, store] where: Store_e_nom_extendable expression: Store_e_nom_ext >= Store_e_nom_min ``` ```math -E_{v} \ge \underline{\mathrm{e}}^{\mathrm{nom}}_{v} \qquad \forall\, v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} +E_{v} \ge \underline{\mathrm{e}}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} ``` ### `Store-ext-e_nom-upper` @@ -7375,14 +7520,14 @@ E_{v} \ge \underline{\mathrm{e}}^{\mathrm{nom}}_{v} \qquad \forall\, v \in \math ```yaml Store_ext_e_nom_upper: - description: "`Store-ext-e_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [store] + description: "`Store-ext-e_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, store] where: Store_e_nom_extendable AND Store_e_nom_max expression: Store_e_nom_ext <= Store_e_nom_max ``` ```math -E_{v} \le \overline{\mathrm{e}}^{\mathrm{nom}}_{v} \qquad \forall\, v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \overline{\mathrm{e}}^{\mathrm{nom}}_{v} \text{ is defined} +E_{v} \le \overline{\mathrm{e}}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \overline{\mathrm{e}}^{\mathrm{nom}}_{\xi,v} \text{ is defined} ``` ### `Store-e_nom_set` @@ -7392,13 +7537,13 @@ E_{v} \le \overline{\mathrm{e}}^{\mathrm{nom}}_{v} \qquad \forall\, v \in \mathc ```yaml Store_e_nom_set: description: "`Store-e_nom_set` — the chosen build pinned, wherever a value is given" - dims: [store] + dims: [scenario, store] where: Store_e_nom_extendable AND Store_e_nom_set expression: Store_e_nom_ext == Store_e_nom_set ``` ```math -E_{v} = \mathrm{e}^{\mathrm{nom,set}}_{v} \qquad \forall\, v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{e}^{\mathrm{nom,set}}_{v} \text{ is defined} +E_{v} = \mathrm{e}^{\mathrm{nom,set}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{e}^{\mathrm{nom,set}}_{\xi,v} \text{ is defined} ``` ### `Store-energy_balance` @@ -7433,7 +7578,7 @@ Generator_p_set: ``` ```math -p_{\xi,t,g} = \mathrm{p}^{\mathrm{set}}_{t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{p}^{\mathrm{set}}_{t,g} \text{ is defined} \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} = \mathrm{p}^{\mathrm{set}}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{p}^{\mathrm{set}}_{\xi,t,g} \text{ is defined} \wedge \mathrm{on}_{t,g} ``` ### `Link-p_set` @@ -7449,7 +7594,7 @@ Link_p_set: ``` ```math -f_{\xi,t,l} = \mathrm{f}^{\mathrm{set}}_{t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{f}^{\mathrm{set}}_{t,l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} = \mathrm{f}^{\mathrm{set}}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{f}^{\mathrm{set}}_{\xi,t,l} \text{ is defined} \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-p_set` @@ -7465,7 +7610,7 @@ Process_p_set: ``` ```math -z_{\xi,t,j} = \mathrm{z}^{\mathrm{set}}_{t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{z}^{\mathrm{set}}_{t,j} \text{ is defined} \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} = \mathrm{z}^{\mathrm{set}}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{z}^{\mathrm{set}}_{\xi,t,j} \text{ is defined} \wedge \mathrm{on}^{z}_{t,j} ``` ### `StorageUnit-p_set` @@ -7481,7 +7626,7 @@ StorageUnit_p_set: ``` ```math -h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} = \mathrm{h}^{\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} +h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} = \mathrm{h}^{\mathrm{set}}_{\xi,t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{\mathrm{set}}_{\xi,t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-p_dispatch_set` @@ -7497,7 +7642,7 @@ StorageUnit_p_dispatch_set: ``` ```math -h^{+}_{\xi,t,s} = \mathrm{h}^{+,\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{+,\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} +h^{+}_{\xi,t,s} = \mathrm{h}^{+,\mathrm{set}}_{\xi,t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{+,\mathrm{set}}_{\xi,t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-p_store_set` @@ -7513,7 +7658,7 @@ StorageUnit_p_store_set: ``` ```math -h^{-}_{\xi,t,s} = \mathrm{h}^{-,\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{-,\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} +h^{-}_{\xi,t,s} = \mathrm{h}^{-,\mathrm{set}}_{\xi,t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{h}^{-,\mathrm{set}}_{\xi,t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} ``` ### `StorageUnit-state_of_charge_set` @@ -7529,7 +7674,7 @@ StorageUnit_state_of_charge_set: ``` ```math -\mathit{soc}_{\xi,t,s} = \mathrm{soc}^{\mathrm{set}}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{soc}^{\mathrm{set}}_{t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} +\mathit{soc}_{\xi,t,s} = \mathrm{soc}^{\mathrm{set}}_{\xi,t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{soc}^{\mathrm{set}}_{\xi,t,s} \text{ is defined} \wedge \mathrm{on}^{h}_{t,s} ``` ### `Store-e_set` @@ -7545,7 +7690,7 @@ Store_e_set: ``` ```math -e_{\xi,t,v} = \mathrm{e}^{\mathrm{set}}_{t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{e}^{\mathrm{set}}_{t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} +e_{\xi,t,v} = \mathrm{e}^{\mathrm{set}}_{\xi,t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{e}^{\mathrm{set}}_{\xi,t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} ``` ### `Store-p_set` @@ -7561,7 +7706,7 @@ Store_p_set: ``` ```math -q_{\xi,t,v} = \mathrm{q}^{\mathrm{set}}_{t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{q}^{\mathrm{set}}_{t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} +q_{\xi,t,v} = \mathrm{q}^{\mathrm{set}}_{\xi,t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{q}^{\mathrm{set}}_{\xi,t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} ``` ### `primary_energy` @@ -7673,7 +7818,7 @@ GlobalConstraint_transmission_volume_expansion_limit_ub: ``` ```math -\mathit{transmission\_volume\_expansion}_{i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} +\mathit{transmission\_volume\_expansion}_{\xi,i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -7689,7 +7834,7 @@ GlobalConstraint_transmission_volume_expansion_limit_lb: ``` ```math -\mathit{transmission\_volume\_expansion}_{i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} +\mathit{transmission\_volume\_expansion}_{\xi,i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} ``` ### `transmission_volume_expansion_limit` @@ -7705,7 +7850,7 @@ GlobalConstraint_transmission_volume_expansion_limit_eq: ``` ```math -\mathit{transmission\_volume\_expansion}_{i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} +\mathit{transmission\_volume\_expansion}_{\xi,i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_volume\_expansion\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -7721,7 +7866,7 @@ GlobalConstraint_transmission_expansion_cost_limit_ub: ``` ```math -\mathit{transmission\_expansion\_cost}_{i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} +\mathit{transmission\_expansion\_cost}_{\xi,i} \le \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{<=}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -7737,7 +7882,7 @@ GlobalConstraint_transmission_expansion_cost_limit_lb: ``` ```math -\mathit{transmission\_expansion\_cost}_{i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} +\mathit{transmission\_expansion\_cost}_{\xi,i} \ge \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{>=}\text{'} ``` ### `transmission_expansion_cost_limit` @@ -7753,7 +7898,7 @@ GlobalConstraint_transmission_expansion_cost_limit_eq: ``` ```math -\mathit{transmission\_expansion\_cost}_{i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} +\mathit{transmission\_expansion\_cost}_{\xi,i} = \mathrm{K}_{\xi,i} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} \,:\, \mathrm{type}_{i} = \text{'}\mathrm{transmission\_expansion\_cost\_limit}\text{'} \wedge \mathrm{sense}_{\xi,i} = \text{'}\mathrm{==}\text{'} ``` ### `tech_capacity_expansion_limit` @@ -7912,7 +8057,7 @@ Generator_previous_status: ``` ```math -\overleftarrow{u}_{\xi,t,g} = \begin{cases} \mathrm{u}^{0}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ u_{\xi,t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\overleftarrow{u}_{\xi,t,g} = \begin{cases} \mathrm{u}^{0}_{\xi,g} & \text{if } \mathrm{pos}(t) = 0 \\ u_{\xi,t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_previous_p` @@ -7938,14 +8083,14 @@ Generator_previous_p: ```yaml Generator_p_nom_effective: description: the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise - dims: [generator] + dims: [scenario, generator] cases: extendable: { when: Generator_p_nom_extendable, expression: Generator_p_nom_ext } otherwise: Generator_p_nom ``` ```math -\widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} = \begin{cases} P_{g} & \text{if } \mathrm{ext}_{g} \\ \mathrm{p}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widetilde{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} = \begin{cases} P_{g} & \text{if } \mathrm{ext}_{g} \\ \mathrm{p}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} ``` ### `Generator_ramp_up_rate` @@ -7955,14 +8100,14 @@ Generator_ramp_up_rate: description: >- the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}_{g} = \begin{cases} \mathrm{ru}_{g} & \text{if } \mathrm{ru}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widetilde{\mathrm{ru}}_{\xi,g} = \begin{cases} \mathrm{ru}_{\xi,g} & \text{if } \mathrm{ru}_{\xi,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} ``` ### `Generator_ramp_down_rate` @@ -7972,14 +8117,14 @@ Generator_ramp_down_rate: description: >- the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}_{g} = \begin{cases} \mathrm{rd}_{g} & \text{if } \mathrm{rd}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widetilde{\mathrm{rd}}_{\xi,g} = \begin{cases} \mathrm{rd}_{\xi,g} & \text{if } \mathrm{rd}_{\xi,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} ``` ### `Generator_start_up_rate` @@ -7989,14 +8134,14 @@ Generator_start_up_rate: description: >- the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_start_up, expression: Generator_ramp_limit_start_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} = \begin{cases} \mathrm{ru}^{\mathrm{up}}_{g} & \text{if } \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} = \begin{cases} \mathrm{ru}^{\mathrm{up}}_{\xi,g} & \text{if } \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} ``` ### `Generator_shut_down_rate` @@ -8006,14 +8151,14 @@ Generator_shut_down_rate: description: >- the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_shut_down, expression: Generator_ramp_limit_shut_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} = \begin{cases} \mathrm{rd}^{\mathrm{dn}}_{g} & \text{if } \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} = \begin{cases} \mathrm{rd}^{\mathrm{dn}}_{\xi,g} & \text{if } \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} ``` ### `Generator_p_nom_committed` @@ -8023,14 +8168,14 @@ Generator_p_nom_committed: description: >- the build a committed unit's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise - dims: [generator] + dims: [scenario, generator] cases: modular_build: { when: Generator_p_nom_extendable AND Generator_p_nom_mod > 0, expression: Generator_p_nom_mod } otherwise: Generator_p_nom ``` ```math -\widehat{\mathrm{p}}^{\mathrm{nom}}_{g} = \begin{cases} \mathrm{p}^{\mathrm{mod}}_{g} & \text{if } \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \\ \mathrm{p}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} = \begin{cases} \mathrm{p}^{\mathrm{mod}}_{g} & \text{if } \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 \\ \mathrm{p}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} ``` ### `Generator_ramp_up_allowance` @@ -8053,7 +8198,7 @@ Generator_ramp_up_allowance: ``` ```math -\Delta^{+}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{ru}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{\xi,t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{ru}}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{+}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{ru}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{ru}}_{\xi,g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_ramp_down_allowance` @@ -8076,7 +8221,7 @@ Generator_ramp_down_allowance: ``` ```math -\Delta^{-}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{rd}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot u_{\xi,t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{rd}}_{g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{-}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{rd}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot u_{\xi,t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{rd}}_{\xi,g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Link_p_nom_effective` @@ -8084,14 +8229,14 @@ Generator_ramp_down_allowance: ```yaml Link_p_nom_effective: description: the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise - dims: [link] + dims: [scenario, link] cases: extendable: { when: Link_p_nom_extendable, expression: Link_p_nom_ext } otherwise: Link_p_nom ``` ```math -\widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} = \begin{cases} F_{l} & \text{if } \mathrm{ext}^{f}_{l} \\ \mathrm{f}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +\widetilde{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} = \begin{cases} F_{l} & \text{if } \mathrm{ext}^{f}_{l} \\ \mathrm{f}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} ``` ### `Link_previous_status` @@ -8108,7 +8253,7 @@ Link_previous_status: ``` ```math -\overleftarrow{u}^{f}_{\xi,t,l} = \begin{cases} \mathrm{u}^{f,0}_{l} & \text{if } \mathrm{pos}(t) = 0 \\ u^{f}_{\xi,t - 1,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\overleftarrow{u}^{f}_{\xi,t,l} = \begin{cases} \mathrm{u}^{f,0}_{\xi,l} & \text{if } \mathrm{pos}(t) = 0 \\ u^{f}_{\xi,t - 1,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Link_previous_p` @@ -8136,14 +8281,14 @@ Link_ramp_up_rate: description: >- the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_up, expression: Link_ramp_limit_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}^{f}_{l} = \begin{cases} \mathrm{ru}^{f}_{l} & \text{if } \mathrm{ru}^{f}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +\widetilde{\mathrm{ru}}^{f}_{\xi,l} = \begin{cases} \mathrm{ru}^{f}_{\xi,l} & \text{if } \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} ``` ### `Link_ramp_down_rate` @@ -8153,14 +8298,14 @@ Link_ramp_down_rate: description: >- the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_down, expression: Link_ramp_limit_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}^{f}_{l} = \begin{cases} \mathrm{rd}^{f}_{l} & \text{if } \mathrm{rd}^{f}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +\widetilde{\mathrm{rd}}^{f}_{\xi,l} = \begin{cases} \mathrm{rd}^{f}_{\xi,l} & \text{if } \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} ``` ### `Link_start_up_rate` @@ -8170,14 +8315,14 @@ Link_start_up_rate: description: >- the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_start_up, expression: Link_ramp_limit_start_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} = \begin{cases} \mathrm{ru}^{f,\mathrm{up}}_{l} & \text{if } \mathrm{ru}^{f,\mathrm{up}}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +\widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} = \begin{cases} \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} & \text{if } \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} ``` ### `Link_shut_down_rate` @@ -8187,14 +8332,14 @@ Link_shut_down_rate: description: >- the shut-down ramp a link's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_shut_down, expression: Link_ramp_limit_shut_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} = \begin{cases} \mathrm{rd}^{f,\mathrm{dn}}_{l} & \text{if } \mathrm{rd}^{f,\mathrm{dn}}_{l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} = \begin{cases} \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} & \text{if } \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} ``` ### `Link_p_nom_committed` @@ -8204,14 +8349,14 @@ Link_p_nom_committed: description: >- the build a committed link's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise - dims: [link] + dims: [scenario, link] cases: modular_build: { when: Link_p_nom_extendable AND Link_p_nom_mod > 0, expression: Link_p_nom_mod } otherwise: Link_p_nom ``` ```math -\widehat{\mathrm{f}}^{\mathrm{nom}}_{l} = \begin{cases} \mathrm{f}^{\mathrm{mod}}_{l} & \text{if } \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \\ \mathrm{f}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, l \in \mathcal{L} +\widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} = \begin{cases} \mathrm{f}^{\mathrm{mod}}_{l} & \text{if } \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 \\ \mathrm{f}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} ``` ### `Link_ramp_up_allowance` @@ -8234,7 +8379,7 @@ Link_ramp_up_allowance: ``` ```math -\Delta^{f,+}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{ru}}^{f}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} + \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{ru}}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\Delta^{f,+}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{ru}}^{f}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} + \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{ru}}^{f}_{\xi,l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Link_ramp_down_allowance` @@ -8257,7 +8402,7 @@ Link_ramp_down_allowance: ``` ```math -\Delta^{f,-}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{rd}}^{f}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot u^{f}_{\xi,t,l} + \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{l} \cdot \left( \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{rd}}^{f}_{l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\Delta^{f,-}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{rd}}^{f}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot u^{f}_{\xi,t,l} + \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{rd}}^{f}_{\xi,l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Process_p_nom_effective` @@ -8265,14 +8410,14 @@ Link_ramp_down_allowance: ```yaml Process_p_nom_effective: description: the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise - dims: [process] + dims: [scenario, process] cases: extendable: { when: Process_p_nom_extendable, expression: Process_p_nom_ext } otherwise: Process_p_nom ``` ```math -\widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} = \begin{cases} Z_{j} & \text{if } \mathrm{ext}^{z}_{j} \\ \mathrm{z}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +\widetilde{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} = \begin{cases} Z_{j} & \text{if } \mathrm{ext}^{z}_{j} \\ \mathrm{z}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} ``` ### `Process_previous_status` @@ -8289,7 +8434,7 @@ Process_previous_status: ``` ```math -\overleftarrow{u}^{z}_{\xi,t,j} = \begin{cases} \mathrm{u}^{z,0}_{j} & \text{if } \mathrm{pos}(t) = 0 \\ u^{z}_{\xi,t - 1,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\overleftarrow{u}^{z}_{\xi,t,j} = \begin{cases} \mathrm{u}^{z,0}_{\xi,j} & \text{if } \mathrm{pos}(t) = 0 \\ u^{z}_{\xi,t - 1,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `Process_previous_p` @@ -8317,14 +8462,14 @@ Process_ramp_up_rate: description: >- the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_up, expression: Process_ramp_limit_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}^{z}_{j} = \begin{cases} \mathrm{ru}^{z}_{j} & \text{if } \mathrm{ru}^{z}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +\widetilde{\mathrm{ru}}^{z}_{\xi,j} = \begin{cases} \mathrm{ru}^{z}_{\xi,j} & \text{if } \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} ``` ### `Process_ramp_down_rate` @@ -8334,14 +8479,14 @@ Process_ramp_down_rate: description: >- the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_down, expression: Process_ramp_limit_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}^{z}_{j} = \begin{cases} \mathrm{rd}^{z}_{j} & \text{if } \mathrm{rd}^{z}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +\widetilde{\mathrm{rd}}^{z}_{\xi,j} = \begin{cases} \mathrm{rd}^{z}_{\xi,j} & \text{if } \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} ``` ### `Process_start_up_rate` @@ -8351,14 +8496,14 @@ Process_start_up_rate: description: >- the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_start_up, expression: Process_ramp_limit_start_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} = \begin{cases} \mathrm{ru}^{z,\mathrm{up}}_{j} & \text{if } \mathrm{ru}^{z,\mathrm{up}}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +\widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} = \begin{cases} \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} & \text{if } \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} ``` ### `Process_shut_down_rate` @@ -8368,14 +8513,14 @@ Process_shut_down_rate: description: >- the shut-down ramp a process's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_shut_down, expression: Process_ramp_limit_shut_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} = \begin{cases} \mathrm{rd}^{z,\mathrm{dn}}_{j} & \text{if } \mathrm{rd}^{z,\mathrm{dn}}_{j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} = \begin{cases} \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} & \text{if } \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} ``` ### `Process_p_nom_committed` @@ -8385,14 +8530,14 @@ Process_p_nom_committed: description: >- the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise - dims: [process] + dims: [scenario, process] cases: modular_build: { when: Process_p_nom_extendable AND Process_p_nom_mod > 0, expression: Process_p_nom_mod } otherwise: Process_p_nom ``` ```math -\widehat{\mathrm{z}}^{\mathrm{nom}}_{j} = \begin{cases} \mathrm{z}^{\mathrm{mod}}_{j} & \text{if } \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \\ \mathrm{z}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, j \in \mathcal{J} +\widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} = \begin{cases} \mathrm{z}^{\mathrm{mod}}_{j} & \text{if } \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 \\ \mathrm{z}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} ``` ### `Process_ramp_up_allowance` @@ -8415,7 +8560,7 @@ Process_ramp_up_allowance: ``` ```math -\Delta^{z,+}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{ru}}^{z}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} + \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{ru}}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\Delta^{z,+}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{ru}}^{z}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} + \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{ru}}^{z}_{\xi,j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `Process_ramp_down_allowance` @@ -8438,7 +8583,7 @@ Process_ramp_down_allowance: ``` ```math -\Delta^{z,-}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{rd}}^{z}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot u^{z}_{\xi,t,j} + \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{j} \cdot \left( \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{rd}}^{z}_{j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\Delta^{z,-}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{rd}}^{z}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot u^{z}_{\xi,t,j} + \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{rd}}^{z}_{\xi,j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `StorageUnit_charge_carried_in` @@ -8483,7 +8628,7 @@ StorageUnit_charge_carried_in: ``` ```math -\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{t,s} \cdot \mathit{soc}_{\xi,\left( t \ominus \mathrm{idle} \right) \ominus 1,s} & \text{if } \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \mathrm{soc}^{0}_{s} & \text{if } \neg \mathrm{cyc}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \neg \mathrm{reset}_{s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{s} \\ \mathrm{soc}^{0}_{s} & \text{if } \mathrm{reset}_{s} \wedge \neg \mathrm{cyc}^{y}_{s} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho_{t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} +\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,\left( t \ominus \mathrm{idle} \right) \ominus 1,s} & \text{if } \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \neg \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{\xi,s} \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \mathrm{reset}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} ``` ### `Store_energy_carried_in` @@ -8522,7 +8667,7 @@ Store_energy_carried_in: ``` ```math -\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{v} & \text{if } \neg \mathrm{cyc}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{v} \\ \mathrm{e}^{0}_{v} & \text{if } \mathrm{reset}^{e}_{v} \wedge \neg \mathrm{cyc}^{e,y}_{v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} +\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{\xi,t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \neg \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{\xi,v} \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} ``` ### `Link_output_arrival` @@ -8545,7 +8690,7 @@ Link_output_arrival: ``` ```math -\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} +\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} ``` ### `Process_output_arrival` @@ -8568,7 +8713,7 @@ Process_output_arrival: ``` ```math -\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} +\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} ``` ### `GlobalConstraint_energy_weight` @@ -8579,7 +8724,7 @@ GlobalConstraint_energy_weight: what one unit of power at a snapshot counts for in a row — the generator weighting times the years of the snapshot's period, where the row counts the snapshot, and nothing where it does not - dims: [global_constraint, snapshot] + dims: [scenario, global_constraint, snapshot] cases: counted: when: GlobalConstraint_counts_snapshot @@ -8588,7 +8733,7 @@ GlobalConstraint_energy_weight: ``` ```math -\mathit{w}^{\mathrm{gc}}_{i,t} = \begin{cases} \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{in}_{i,t} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T} +\mathit{w}^{\mathrm{gc}}_{\xi,i,t} = \begin{cases} \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{in}_{\xi,i,t} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T} ``` ### `GlobalConstraint_snapshot_closes` @@ -8596,7 +8741,7 @@ GlobalConstraint_energy_weight: ```yaml GlobalConstraint_snapshot_closes: description: one at the last snapshot a row counts, and zero elsewhere - dims: [global_constraint, snapshot] + dims: [scenario, global_constraint, snapshot] cases: last_counted: when: GlobalConstraint_counts_snapshot AND NOT shift(GlobalConstraint_counts_snapshot, along=snapshot, offset=-1) @@ -8605,7 +8750,7 @@ GlobalConstraint_snapshot_closes: ``` ```math -\mathit{last}_{i,t} = \begin{cases} 1 & \text{if } \mathrm{in}_{i,t} \wedge \neg \mathrm{in}_{i,t + 1} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T} +\mathit{last}_{\xi,i,t} = \begin{cases} 1 & \text{if } \mathrm{in}_{\xi,i,t} \wedge \neg \mathrm{in}_{\xi,i,t + 1} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T} ``` ### `StorageUnit_closing_weight` @@ -8617,7 +8762,7 @@ StorageUnit_closing_weight: closing level — the years of the period at the last snapshot of each counted period where the unit reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere - dims: [global_constraint, snapshot, storage_unit] + dims: [scenario, global_constraint, snapshot, storage_unit] cases: per_period: when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 @@ -8629,7 +8774,7 @@ StorageUnit_closing_weight: ``` ```math -\mathit{w}^{h}_{i,t,s} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}_{s} \wedge \mathrm{in}_{i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}_{i,t} & \text{if } \neg \mathrm{reset}_{s} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T},\ s \in \mathcal{S} +\mathit{w}^{h}_{\xi,i,t,s} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}_{\xi,i,t} & \text{if } \neg \mathrm{reset}_{\xi,s} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T},\ s \in \mathcal{S} ``` ### `Store_closing_weight` @@ -8641,7 +8786,7 @@ Store_closing_weight: closing level — the years of the period at the last snapshot of each counted period where the store reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere - dims: [global_constraint, snapshot, store] + dims: [scenario, global_constraint, snapshot, store] cases: per_period: when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 @@ -8653,7 +8798,7 @@ Store_closing_weight: ``` ```math -\mathit{w}^{e}_{i,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}^{e}_{v} \wedge \mathrm{in}_{i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}_{i,t} & \text{if } \neg \mathrm{reset}^{e}_{v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T},\ v \in \mathcal{V} +\mathit{w}^{e}_{\xi,i,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}_{\xi,i,t} & \text{if } \neg \mathrm{reset}^{e}_{\xi,v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T},\ v \in \mathcal{V} ``` ### `primary_energy` @@ -8672,7 +8817,7 @@ primary_energy: ``` ```math -\mathit{primary\_energy}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathit{w}^{\mathrm{gc}}_{i,t} \cdot \mathrm{a}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathit{w}^{h}_{i,t,s} \cdot \mathrm{a}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{i,t,v} \cdot \mathrm{a}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} +\mathit{primary\_energy}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathit{w}^{\mathrm{gc}}_{\xi,i,t} \cdot \mathrm{a}_{\xi,i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathit{w}^{h}_{\xi,i,t,s} \cdot \mathrm{a}^{h}_{\xi,i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{\xi,i,t,v} \cdot \mathrm{a}^{e}_{\xi,i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `operational_limit` @@ -8691,7 +8836,7 @@ operational_limit: ``` ```math -\mathit{operational\_limit}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathit{w}^{\mathrm{gc}}_{i,t} \cdot \mathrm{b}_{i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathit{w}^{h}_{i,t,s} \cdot \mathrm{b}^{h}_{i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{i,t,v} \cdot \mathrm{b}^{e}_{i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} +\mathit{operational\_limit}_{\xi,i} = \sum_{g \in \mathcal{G}} \sum_{t \in \mathcal{T}} p_{\xi,t,g} \cdot \mathit{w}^{\mathrm{gc}}_{\xi,i,t} \cdot \mathrm{b}_{\xi,i,g} - \left( \sum_{s \in \mathcal{S}} \sum_{t \in \mathcal{T}} \mathit{soc}_{\xi,t,s} \cdot \mathit{w}^{h}_{\xi,i,t,s} \cdot \mathrm{b}^{h}_{\xi,i,s} \right) - \left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{\xi,i,t,v} \cdot \mathrm{b}^{e}_{\xi,i,v} \right) \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `transmission_volume_expansion` @@ -8705,7 +8850,7 @@ transmission_volume_expansion: ``` ```math -\mathit{transmission\_volume\_expansion}_{i} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{len}_{i,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{len}^{f}_{i,l} \qquad \forall\, i \in \mathcal{I} +\mathit{transmission\_volume\_expansion}_{\xi,i} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{len}_{\xi,i,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{len}^{f}_{\xi,i,l} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `transmission_expansion_cost` @@ -8719,7 +8864,7 @@ transmission_expansion_cost: ``` ```math -\mathit{transmission\_expansion\_cost}_{i} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{cc}_{i,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{cc}^{f}_{i,l} \qquad \forall\, i \in \mathcal{I} +\mathit{transmission\_expansion\_cost}_{\xi,i} = \sum_{k \in \mathcal{K}} S_{k} \cdot \mathrm{cc}_{\xi,i,k} + \sum_{l \in \mathcal{L}} F_{l} \cdot \mathrm{cc}^{f}_{\xi,i,l} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I} ``` ### `tech_capacity_expansion` @@ -8771,7 +8916,7 @@ scenario_opex: ``` ```math -\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{\xi,g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{\xi,g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{\xi,l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{\xi,l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{\xi,j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{\xi,j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Carrier_additions` @@ -8905,7 +9050,7 @@ h^{-}_{\xi,t,s} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T}, **`StorageUnit_spill`** ```math -0 \le \mathit{spill}_{\xi,t,s} \le \mathrm{inflow}_{t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{inflow}_{t,s} > 0 \wedge \mathrm{on}^{h}_{t,s} +0 \le \mathit{spill}_{\xi,t,s} \le \mathrm{inflow}_{\xi,t,s} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \mathrm{inflow}_{\xi,t,s} > 0 \wedge \mathrm{on}^{h}_{t,s} ``` **`Store_e`** @@ -9166,7 +9311,7 @@ Generator_maintenance_events_positive: ``` ```math -\mathrm{n}^{\mathrm{mnt}}_{g} > 0 \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +\mathrm{n}^{\mathrm{mnt}}_{\xi,g} > 0 \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} ``` ### `Generator_maintenance_duration_positive` @@ -9181,7 +9326,7 @@ Generator_maintenance_duration_positive: ``` ```math -\tau^{\mathrm{mnt}}_{g} > 0 \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +\tau^{\mathrm{mnt}}_{\xi,g} > 0 \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} ``` ### `Generator_maintenance_duration_fits_the_horizon` @@ -9197,7 +9342,7 @@ Generator_maintenance_duration_fits_the_horizon: ``` ```math -\tau^{\mathrm{mnt}}_{g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +\tau^{\mathrm{mnt}}_{\xi,g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} ``` ### `Generator_maintenance_events_fit_the_horizon` @@ -9213,7 +9358,7 @@ Generator_maintenance_events_fit_the_horizon: ``` ```math -\tau^{\mathrm{mnt}}_{g} \cdot \mathrm{n}^{\mathrm{mnt}}_{g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +\tau^{\mathrm{mnt}}_{\xi,g} \cdot \mathrm{n}^{\mathrm{mnt}}_{\xi,g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} ``` ### `Generator_maintenance_build_cap_is_finite` @@ -9229,7 +9374,7 @@ Generator_maintenance_build_cap_is_finite: ``` ```math -\overline{\mathrm{p}}^{\mathrm{nom}}_{g} < \infty \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} +\overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} < \infty \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{ext}_{g} ``` ### `Generator_maintenance_module_count_is_finite` @@ -9246,7 +9391,7 @@ Generator_maintenance_module_count_is_finite: ``` ```math -\overline{\mathrm{p}}^{\mathrm{nom}}_{g} < \infty \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 +\overline{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} < \infty \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} \wedge \neg \mathrm{ext}_{g} \wedge \mathrm{p}^{\mathrm{mod}}_{g} > 0 ``` ### `Link_maintenance_events_positive` @@ -9261,7 +9406,7 @@ Link_maintenance_events_positive: ``` ```math -\mathrm{n}^{f,\mathrm{mnt}}_{l} > 0 \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +\mathrm{n}^{f,\mathrm{mnt}}_{\xi,l} > 0 \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} ``` ### `Link_maintenance_duration_positive` @@ -9276,7 +9421,7 @@ Link_maintenance_duration_positive: ``` ```math -\tau^{f,\mathrm{mnt}}_{l} > 0 \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +\tau^{f,\mathrm{mnt}}_{\xi,l} > 0 \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} ``` ### `Link_maintenance_duration_fits_the_horizon` @@ -9292,7 +9437,7 @@ Link_maintenance_duration_fits_the_horizon: ``` ```math -\tau^{f,\mathrm{mnt}}_{l} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +\tau^{f,\mathrm{mnt}}_{\xi,l} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} ``` ### `Link_maintenance_events_fit_the_horizon` @@ -9308,7 +9453,7 @@ Link_maintenance_events_fit_the_horizon: ``` ```math -\tau^{f,\mathrm{mnt}}_{l} \cdot \mathrm{n}^{f,\mathrm{mnt}}_{l} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} +\tau^{f,\mathrm{mnt}}_{\xi,l} \cdot \mathrm{n}^{f,\mathrm{mnt}}_{\xi,l} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} ``` ### `Link_maintenance_build_cap_is_finite` @@ -9324,7 +9469,7 @@ Link_maintenance_build_cap_is_finite: ``` ```math -\overline{\mathrm{f}}^{\mathrm{nom}}_{l} < \infty \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} +\overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} < \infty \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} ``` ### `Link_maintenance_module_count_is_finite` @@ -9341,7 +9486,7 @@ Link_maintenance_module_count_is_finite: ``` ```math -\overline{\mathrm{f}}^{\mathrm{nom}}_{l} < \infty \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 +\overline{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} < \infty \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} \,:\, \mathrm{mnt}^{f}_{l} \wedge \mathrm{com}^{f}_{l} \wedge \neg \mathrm{ext}^{f}_{l} \wedge \mathrm{f}^{\mathrm{mod}}_{l} > 0 ``` ### `Process_maintenance_events_positive` @@ -9356,7 +9501,7 @@ Process_maintenance_events_positive: ``` ```math -\mathrm{n}^{z,\mathrm{mnt}}_{j} > 0 \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +\mathrm{n}^{z,\mathrm{mnt}}_{\xi,j} > 0 \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} ``` ### `Process_maintenance_duration_positive` @@ -9371,7 +9516,7 @@ Process_maintenance_duration_positive: ``` ```math -\tau^{z,\mathrm{mnt}}_{j} > 0 \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +\tau^{z,\mathrm{mnt}}_{\xi,j} > 0 \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} ``` ### `Process_maintenance_duration_fits_the_horizon` @@ -9387,7 +9532,7 @@ Process_maintenance_duration_fits_the_horizon: ``` ```math -\tau^{z,\mathrm{mnt}}_{j} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +\tau^{z,\mathrm{mnt}}_{\xi,j} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} ``` ### `Process_maintenance_events_fit_the_horizon` @@ -9403,7 +9548,7 @@ Process_maintenance_events_fit_the_horizon: ``` ```math -\tau^{z,\mathrm{mnt}}_{j} \cdot \mathrm{n}^{z,\mathrm{mnt}}_{j} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} +\tau^{z,\mathrm{mnt}}_{\xi,j} \cdot \mathrm{n}^{z,\mathrm{mnt}}_{\xi,j} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} ``` ### `Process_maintenance_build_cap_is_finite` @@ -9419,7 +9564,7 @@ Process_maintenance_build_cap_is_finite: ``` ```math -\overline{\mathrm{z}}^{\mathrm{nom}}_{j} < \infty \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} +\overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} < \infty \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} ``` ### `Process_maintenance_module_count_is_finite` @@ -9436,7 +9581,7 @@ Process_maintenance_module_count_is_finite: ``` ```math -\overline{\mathrm{z}}^{\mathrm{nom}}_{j} < \infty \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 +\overline{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} < \infty \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} \,:\, \mathrm{mnt}^{z}_{j} \wedge \mathrm{com}^{z}_{j} \wedge \neg \mathrm{ext}^{z}_{j} \wedge \mathrm{z}^{\mathrm{mod}}_{j} > 0 ``` ### `StorageUnit_stands_in_one_run` @@ -9546,7 +9691,7 @@ StorageUnit_primary_energy_per_period_closes_over_the_horizon: ``` ```math -\lvert \{ t \in \mathcal{T} \,:\, \neg \mathrm{in}_{i,t} \} \rvert = 0 \qquad \forall\, s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{a}^{h}_{i,s} \text{ is defined} \wedge \mathrm{reset}_{s} +\lvert \{ t \in \mathcal{T} \,:\, \neg \mathrm{in}_{\xi,i,t} \} \rvert = 0 \qquad \forall\, \xi \in \Xi,\ s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{a}^{h}_{\xi,i,s} \text{ is defined} \wedge \mathrm{reset}_{\xi,s} ``` ### `StorageUnit_primary_energy_carried_over_has_unit_years` @@ -9563,7 +9708,7 @@ StorageUnit_primary_energy_carried_over_has_unit_years: ``` ```math -\mathrm{w}^{\mathrm{yr}}_{y} = 1 \qquad \forall\, s \in \mathcal{S},\ i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \mathrm{a}^{h}_{i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{s} +\mathrm{w}^{\mathrm{yr}}_{y} = 1 \qquad \forall\, \xi \in \Xi,\ s \in \mathcal{S},\ i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \mathrm{a}^{h}_{\xi,i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{\xi,s} ``` ### `StorageUnit_operational_limit_carried_over_has_unit_years` @@ -9578,7 +9723,7 @@ StorageUnit_operational_limit_carried_over_has_unit_years: ``` ```math -\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{b}^{h}_{i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{s} \wedge \mathrm{in}_{i,t} +\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{b}^{h}_{\xi,i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,i,t} ``` ### `Store_primary_energy_per_period_closes_over_the_horizon` @@ -9594,7 +9739,7 @@ Store_primary_energy_per_period_closes_over_the_horizon: ``` ```math -\lvert \{ t \in \mathcal{T} \,:\, \neg \mathrm{in}_{i,t} \} \rvert = 0 \qquad \forall\, v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{a}^{e}_{i,v} \text{ is defined} \wedge \mathrm{reset}^{e}_{v} +\lvert \{ t \in \mathcal{T} \,:\, \neg \mathrm{in}_{\xi,i,t} \} \rvert = 0 \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{a}^{e}_{\xi,i,v} \text{ is defined} \wedge \mathrm{reset}^{e}_{\xi,v} ``` ### `Store_primary_energy_carried_over_has_unit_years` @@ -9611,7 +9756,7 @@ Store_primary_energy_carried_over_has_unit_years: ``` ```math -\mathrm{w}^{\mathrm{yr}}_{y} = 1 \qquad \forall\, v \in \mathcal{V},\ i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \mathrm{a}^{e}_{i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{v} +\mathrm{w}^{\mathrm{yr}}_{y} = 1 \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V},\ i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \mathrm{a}^{e}_{\xi,i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} ``` ### `Store_operational_limit_carried_over_has_unit_years` @@ -9626,7 +9771,7 @@ Store_operational_limit_carried_over_has_unit_years: ``` ```math -\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{b}^{e}_{i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{v} \wedge \mathrm{in}_{i,t} +\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{b}^{e}_{\xi,i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,i,t} ``` ### `Generator_marginal_cost_quadratic_without_risk_preference` @@ -9643,7 +9788,7 @@ Generator_marginal_cost_quadratic_without_risk_preference: ``` ```math -\mathrm{c}^{(2)}_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \omega > 0 +\mathrm{c}^{(2)}_{\xi,t,g} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \omega > 0 ``` ### `Link_marginal_cost_quadratic_without_risk_preference` @@ -9660,7 +9805,7 @@ Link_marginal_cost_quadratic_without_risk_preference: ``` ```math -\mathrm{c}^{f,(2)}_{t,l} = 0 \qquad \forall\, t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \omega > 0 +\mathrm{c}^{f,(2)}_{\xi,t,l} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \omega > 0 ``` ### `Process_marginal_cost_quadratic_without_risk_preference` @@ -9677,7 +9822,7 @@ Process_marginal_cost_quadratic_without_risk_preference: ``` ```math -\mathrm{c}^{z,(2)}_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \omega > 0 +\mathrm{c}^{z,(2)}_{\xi,t,j} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \omega > 0 ``` ### `StorageUnit_marginal_cost_quadratic_without_risk_preference` @@ -9694,7 +9839,7 @@ StorageUnit_marginal_cost_quadratic_without_risk_preference: ``` ```math -\mathrm{c}^{h,(2)}_{t,s} = 0 \qquad \forall\, t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \omega > 0 +\mathrm{c}^{h,(2)}_{\xi,t,s} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} \,:\, \omega > 0 ``` ### `Store_marginal_cost_quadratic_without_risk_preference` @@ -9711,7 +9856,7 @@ Store_marginal_cost_quadratic_without_risk_preference: ``` ```math -\mathrm{c}^{q,(2)}_{t,v} = 0 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \omega > 0 +\mathrm{c}^{q,(2)}_{\xi,t,v} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \omega > 0 ``` ### `GlobalConstraint_tech_capacity_expansion_limit_without_scenarios` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index a58374b9..d1cd6321 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -6,14 +6,16 @@ description: >- A plain `n.optimize()`, and its multi-period and stochastic classes, in one file. Every second-stage quantity spans a `scenario` (a future dispatch is chosen in) and every asset stands in the investment `period`s its build year - and lifetime span. Capacity is chosen once, before the future is known, and - paid once per active period; operation is the expectation over the scenarios' - weights, with a share priced at the tail through the CVaR rows, which stand - only where that share is positive. A plain run feeds one scenario, one period, all-active masks and unit weights, and the - model collapses to the standard one. A security-constrained run copies each - branch flow limit once per outage in an `outage` set that a plain run leaves - empty. Which snapshots an asset is active in, a scenario's weight, and the - outage factors are data prep. + and lifetime span. A parameter spans `scenario` exactly when PyPSA reads it + per scenario. Capacity is chosen once, before the future is known, and paid + once per active period at its cost in expectation over the scenarios; + operation is the expectation over the scenarios' weights, with a share priced + at the tail through the CVaR rows, which stand only where that share is + positive. A plain run feeds one scenario, one period, all-active masks and + unit weights, and the model collapses to the standard one. A + security-constrained run copies each branch flow limit once per outage in an + `outage` set that a plain run leaves empty. Which snapshots an asset is + active in, a scenario's weight, and the outage factors are data prep. dimensions: scenario: @@ -76,74 +78,74 @@ parameters: dims: [snapshot] Generator_p_nom: description: nominal power - dims: [generator] + dims: [scenario, generator] Generator_p_nom_extendable: description: whether the nominal power is a decision dims: [generator] dtype: bool Generator_p_min_pu: description: least output, per unit of nominal power - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] Generator_p_max_pu: description: most output, per unit of nominal power — an availability profile - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] Generator_marginal_cost: description: cost of one unit of output - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] Generator_marginal_cost_quadratic: description: cost of the square of one unit of output - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] Generator_committable: description: whether output is gated by an on/off status decision dims: [generator] dtype: bool Generator_ramp_limit_up: description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit - dims: [generator] + dims: [scenario, generator] Generator_ramp_limit_down: description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit - dims: [generator] + dims: [scenario, generator] Generator_ramp_limit_start_up: description: most output in the snapshot a unit starts, per unit of nominal power - dims: [generator] + dims: [scenario, generator] Generator_ramp_limit_shut_down: description: most output in the snapshot before a unit stops, per unit of nominal power - dims: [generator] + dims: [scenario, generator] Generator_min_up_time: description: least snapshots a unit stays on once started - dims: [generator] + dims: [scenario, generator] dtype: int Generator_min_down_time: description: least snapshots a unit stays off once stopped - dims: [generator] + dims: [scenario, generator] dtype: int Generator_status_initial: description: one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep - dims: [generator] + dims: [scenario, generator] dtype: int Generator_must_stay_up: description: >- true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] dtype: bool Generator_must_stay_down: description: >- true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] dtype: bool Generator_start_up_cost: description: cost of one start - dims: [generator] + dims: [scenario, generator] Generator_shut_down_cost: description: cost of one stop - dims: [generator] + dims: [scenario, generator] Generator_stand_by_cost: description: cost of one snapshot spent on - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] Generator_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [generator] @@ -153,26 +155,28 @@ parameters: / Generator_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules - dims: [generator] + dims: [scenario, generator] Generator_big_m: description: a bound safely above any feasible output — the build cap at full availability, data prep - dims: [generator] + dims: [scenario, generator] Generator_p_min_pu_nonneg: description: >- true where none of the generator's own minimums-per-unit is negative — - PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep + PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep dims: [generator] dtype: bool Generator_maintainable: - description: whether a generator must be taken off for maintenance within the horizon + description: >- + whether a generator must be taken off for maintenance within the horizon — in + any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) dims: [generator] dtype: bool Generator_maintenance_pu: description: the share of the build a maintenance event takes off - dims: [generator] + dims: [scenario, generator] Generator_maintenance_events: description: how many maintenance events the horizon holds - dims: [generator] + dims: [scenario, generator] dtype: int Generator_maintenance_duration: description: >- @@ -181,105 +185,108 @@ parameters: maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon - dims: [generator] + dims: [scenario, generator] Generator_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] dtype: bool Link_ramp_limit_up: description: most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit - dims: [link] + dims: [scenario, link] Link_ramp_limit_down: description: most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit - dims: [link] + dims: [scenario, link] Link_p_nom: description: nominal power - dims: [link] + dims: [scenario, link] Link_p_nom_extendable: description: whether the nominal power is a decision dims: [link] dtype: bool Link_p_min_pu: description: least flow, per unit of nominal power — negative for a link that carries both ways - dims: [snapshot, link] + dims: [scenario, snapshot, link] Link_p_max_pu: description: most flow, per unit of nominal power - dims: [snapshot, link] + dims: [scenario, snapshot, link] Link_efficiency: description: >- share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers - dims: [link_output] + dims: [scenario, link_output] Link_output_delay: description: >- snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which - the file states as whole snapshots; zero for a port that delivers at once + the file states as whole snapshots; zero for a port that delivers at once. + One for every scenario: PyPSA groups the ports by delay over all scenarios + and shifts each group in every one (`constraints.py:1269`) dims: [link_output] dtype: int Link_output_cyclic_delay: description: >- whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, - the flow still in transit at each period's first snapshots is lost + the flow still in transit at each period's first snapshots is lost. One + for every scenario, as the delay dims: [link_output] dtype: bool Link_marginal_cost: description: cost of one unit of flow - dims: [snapshot, link] + dims: [scenario, snapshot, link] Link_marginal_cost_quadratic: description: cost of the square of one unit of flow - dims: [snapshot, link] + dims: [scenario, snapshot, link] Link_committable: description: whether flow is gated by an on/off status decision dims: [link] dtype: bool Link_ramp_limit_start_up: description: most flow in the snapshot a link starts, per unit of nominal power - dims: [link] + dims: [scenario, link] Link_ramp_limit_shut_down: description: most flow in the snapshot before a link stops, per unit of nominal power - dims: [link] + dims: [scenario, link] Link_min_up_time: description: least snapshots a link stays on once started - dims: [link] + dims: [scenario, link] dtype: int Link_min_down_time: description: least snapshots a link stays off once stopped - dims: [link] + dims: [scenario, link] dtype: int Link_status_initial: description: one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep - dims: [link] + dims: [scenario, link] dtype: int Link_must_stay_up: description: >- true while the up time a link brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter - dims: [snapshot, link] + dims: [scenario, snapshot, link] dtype: bool Link_must_stay_down: description: >- true while the down time a link brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason - dims: [snapshot, link] + dims: [scenario, snapshot, link] dtype: bool Link_start_up_cost: description: cost of one start - dims: [link] + dims: [scenario, link] Link_shut_down_cost: description: cost of one stop - dims: [link] + dims: [scenario, link] Link_stand_by_cost: description: cost of one snapshot spent on - dims: [snapshot, link] + dims: [scenario, snapshot, link] Link_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [link] @@ -289,26 +296,28 @@ parameters: / Link_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules - dims: [link] + dims: [scenario, link] Link_big_m: description: a bound safely above any feasible flow — the build cap at full availability, data prep - dims: [link] + dims: [scenario, link] Link_p_min_pu_nonneg: description: >- true where none of the link's own minimums-per-unit is negative — - PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep + PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep dims: [link] dtype: bool Link_maintainable: - description: whether a link must be taken off for maintenance within the horizon + description: >- + whether a link must be taken off for maintenance within the horizon — in + any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) dims: [link] dtype: bool Link_maintenance_pu: description: the share of the build a maintenance event takes off - dims: [link] + dims: [scenario, link] Link_maintenance_events: description: how many maintenance events the horizon holds - dims: [link] + dims: [scenario, link] dtype: int Link_maintenance_duration: description: >- @@ -317,122 +326,123 @@ parameters: maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon - dims: [link] + dims: [scenario, link] Link_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the link does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep - dims: [snapshot, link] + dims: [scenario, snapshot, link] dtype: bool Process_p_nom: description: nominal internal power - dims: [process] + dims: [scenario, process] Process_p_nom_extendable: description: whether the nominal internal power is a decision dims: [process] dtype: bool Process_p_min_pu: description: least internal power, per unit of nominal power — negative for a process that runs both ways - dims: [snapshot, process] + dims: [scenario, snapshot, process] Process_p_max_pu: description: most internal power, per unit of nominal power - dims: [snapshot, process] + dims: [scenario, snapshot, process] Process_rate: description: >- the energy a port draws or delivers per unit of internal power, PyPSA's `rate0`, `rate1`, … read long — negative where the port withdraws, positive where it injects; a link is a process whose `bus0` rate is minus one and whose output rates are its efficiencies - dims: [process_output] + dims: [scenario, process_output] Process_output_delay: description: >- snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that - transfers at once + transfers at once. One for every scenario, as a link's dims: [process_output] dtype: int Process_output_cyclic_delay: description: >- whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, - the energy still in transit at each period's first snapshots is lost + the energy still in transit at each period's first snapshots is lost. + One for every scenario, as the delay dims: [process_output] dtype: bool Process_marginal_cost: description: cost of one unit of internal power - dims: [snapshot, process] + dims: [scenario, snapshot, process] Process_marginal_cost_quadratic: description: cost of the square of one unit of internal power - dims: [snapshot, process] + dims: [scenario, snapshot, process] Process_ramp_limit_up: description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit - dims: [process] + dims: [scenario, process] Process_ramp_limit_down: description: most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit - dims: [process] + dims: [scenario, process] Process_p_set: description: a given internal power schedule; a process without one has no row here - dims: [snapshot, process] + dims: [scenario, snapshot, process] Process_p_nom_min: description: least nominal power an extendable process may be built at - dims: [process] + dims: [scenario, process] Process_p_nom_max: description: most nominal power an extendable process may be built at - dims: [process] + dims: [scenario, process] Process_capital_cost: description: cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [process] + dims: [scenario, process] Process_p_nom_set: description: a given nominal power for an extendable process; one without a value has no row here - dims: [process] + dims: [scenario, process] Process_committable: description: whether internal power is gated by an on/off status decision dims: [process] dtype: bool Process_ramp_limit_start_up: description: most internal power in the snapshot a process starts, per unit of nominal power - dims: [process] + dims: [scenario, process] Process_ramp_limit_shut_down: description: most internal power in the snapshot before a process stops, per unit of nominal power - dims: [process] + dims: [scenario, process] Process_min_up_time: description: least snapshots a process stays on once started - dims: [process] + dims: [scenario, process] dtype: int Process_min_down_time: description: least snapshots a process stays off once stopped - dims: [process] + dims: [scenario, process] dtype: int Process_status_initial: description: one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep - dims: [process] + dims: [scenario, process] dtype: int Process_must_stay_up: description: >- true while the up time a process brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter - dims: [snapshot, process] + dims: [scenario, snapshot, process] dtype: bool Process_must_stay_down: description: >- true while the down time a process brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason - dims: [snapshot, process] + dims: [scenario, snapshot, process] dtype: bool Process_start_up_cost: description: cost of one start - dims: [process] + dims: [scenario, process] Process_shut_down_cost: description: cost of one stop - dims: [process] + dims: [scenario, process] Process_stand_by_cost: description: cost of one snapshot spent on - dims: [snapshot, process] + dims: [scenario, snapshot, process] Process_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [process] @@ -442,26 +452,28 @@ parameters: / Process_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules - dims: [process] + dims: [scenario, process] Process_big_m: description: a bound safely above any feasible internal power — the build cap at full availability, data prep - dims: [process] + dims: [scenario, process] Process_p_min_pu_nonneg: description: >- true where none of the process's own minimums-per-unit is negative — - PyPSA's per-unit `(p_min_pu >= 0).all()`, data prep + PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep dims: [process] dtype: bool Process_maintainable: - description: whether a process must be taken off for maintenance within the horizon + description: >- + whether a process must be taken off for maintenance within the horizon — in + any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) dims: [process] dtype: bool Process_maintenance_pu: description: the share of the build a maintenance event takes off - dims: [process] + dims: [scenario, process] Process_maintenance_events: description: how many maintenance events the horizon holds - dims: [process] + dims: [scenario, process] dtype: int Process_maintenance_duration: description: >- @@ -470,14 +482,14 @@ parameters: maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon - dims: [process] + dims: [scenario, process] Process_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep - dims: [snapshot, process] + dims: [scenario, snapshot, process] dtype: bool Load_p_set: description: demand @@ -568,17 +580,22 @@ parameters: description: one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep dims: [period, process] Carrier_max_growth: - description: most capacity of a carrier that may be added in a period; no value means no limit + description: >- + most capacity of a carrier that may be added in a period; no value means + no limit. The least over the scenarios, as PyPSA takes it + (`global_constraints.py:226-230`), data prep dims: [carrier] Carrier_max_relative_growth: - description: share of the previous period's additions that may be added on top + description: >- + share of the previous period's additions that may be added on top — the + least over the scenarios, as PyPSA takes it, data prep dims: [carrier] Generator_p_set: description: a given output schedule; a generator without one has no row here - dims: [snapshot, generator] + dims: [scenario, snapshot, generator] Link_p_set: description: a given flow schedule; a link without one has no row here - dims: [snapshot, link] + dims: [scenario, snapshot, link] snapshot_weightings_stores: description: PyPSA's `snapshot_weightings.stores` — hours a snapshot stands for in a storage balance dims: [snapshot] @@ -587,92 +604,92 @@ parameters: dims: [snapshot] Generator_p_nom_min: description: least nominal power an extendable generator may be built at - dims: [generator] + dims: [scenario, generator] Generator_p_nom_max: description: most nominal power an extendable generator may be built at - dims: [generator] + dims: [scenario, generator] Generator_capital_cost: description: cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [generator] + dims: [scenario, generator] Generator_p_nom_set: description: a given nominal power for an extendable generator; one without a value has no row here - dims: [generator] + dims: [scenario, generator] Generator_e_sum_min: description: least energy over the horizon; minus infinity where no floor is meant - dims: [generator] + dims: [scenario, generator] Generator_e_sum_max: description: most energy over the horizon — a fuel or emission budget in energy terms; infinity where no cap is meant - dims: [generator] + dims: [scenario, generator] Link_p_nom_min: description: least nominal power an extendable link may be built at - dims: [link] + dims: [scenario, link] Link_p_nom_max: description: most nominal power an extendable link may be built at - dims: [link] + dims: [scenario, link] Link_capital_cost: description: cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [link] + dims: [scenario, link] Link_p_nom_set: description: a given nominal power for an extendable link; one without a value has no row here - dims: [link] + dims: [scenario, link] StorageUnit_p_nom_min: description: least nominal power an extendable storage unit may be built at - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_p_nom_max: description: most nominal power an extendable storage unit may be built at - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_capital_cost: description: cost of one unit of nominal power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_p_nom_set: description: a given nominal power for an extendable storage unit; one without a value has no row here - dims: [storage_unit] + dims: [scenario, storage_unit] Store_e_nom_min: description: least nominal capacity an extendable store may be built at - dims: [store] + dims: [scenario, store] Store_e_nom_max: description: most nominal capacity an extendable store may be built at - dims: [store] + dims: [scenario, store] Store_capital_cost: description: cost of one unit of nominal capacity — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [store] + dims: [scenario, store] Store_e_nom_set: description: a given nominal capacity for an extendable store; one without a value has no row here - dims: [store] + dims: [scenario, store] StorageUnit_p_nom: description: nominal power - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_p_nom_extendable: description: whether the nominal power is a decision dims: [storage_unit] dtype: bool StorageUnit_p_min_pu: description: most storing, per unit of nominal power and negated - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_p_max_pu: description: most dispatch, per unit of nominal power - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_max_hours: description: energy capacity, as hours of dispatch at nominal power - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_efficiency_store: description: share of the power drawn from the bus that becomes charge - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_efficiency_dispatch: description: share of the charge drawn down that reaches the bus - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_retention: description: share of charge kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_inflow: description: energy arriving per hour, a river into a reservoir - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_state_of_charge_initial: description: charge held before the first snapshot - dims: [storage_unit] + dims: [scenario, storage_unit] StorageUnit_cyclic_state_of_charge: description: whether the horizon closes on itself instead of opening on the initial charge - dims: [storage_unit] + dims: [scenario, storage_unit] dtype: bool StorageUnit_cyclic_state_of_charge_per_period: description: >- @@ -680,14 +697,14 @@ parameters: charge on to the next; it overrides `cyclic_state_of_charge` and `state_of_charge_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise - dims: [storage_unit] + dims: [scenario, storage_unit] dtype: bool StorageUnit_state_of_charge_initial_per_period: description: >- whether each investment period opens on the initial charge instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise - dims: [storage_unit] + dims: [scenario, storage_unit] dtype: bool StorageUnit_opens_late: description: >- @@ -705,50 +722,50 @@ parameters: dtype: int StorageUnit_marginal_cost: description: cost of one unit of dispatch - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_marginal_cost_quadratic: description: cost of the square of one unit of dispatch; storing is not charged - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_marginal_cost_storage: description: cost of one unit of charge held over one snapshot - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_spill_cost: description: cost of one unit of inflow passed on unused - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_p_set: description: a given net dispatch schedule; a unit without one has no row here - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_p_dispatch_set: description: a given dispatch schedule; a unit without one has no row here - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_p_store_set: description: a given charging schedule; a unit without one has no row here - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] StorageUnit_state_of_charge_set: description: a given charge schedule; a unit without one has no row here - dims: [snapshot, storage_unit] + dims: [scenario, snapshot, storage_unit] Store_e_nom: description: nominal energy capacity - dims: [store] + dims: [scenario, store] Store_e_nom_extendable: description: whether the nominal energy capacity is a decision dims: [store] dtype: bool Store_e_min_pu: description: least energy held, per unit of nominal capacity — negative for a store that may go short - dims: [snapshot, store] + dims: [scenario, snapshot, store] Store_e_max_pu: description: most energy held, per unit of nominal capacity - dims: [snapshot, store] + dims: [scenario, snapshot, store] Store_retention: description: share of energy kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep - dims: [snapshot, store] + dims: [scenario, snapshot, store] Store_e_initial: description: energy held before the first snapshot - dims: [store] + dims: [scenario, store] Store_e_cyclic: description: whether the horizon closes on itself instead of opening on the initial energy - dims: [store] + dims: [scenario, store] dtype: bool Store_e_cyclic_per_period: description: >- @@ -756,14 +773,14 @@ parameters: energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise - dims: [store] + dims: [scenario, store] dtype: bool Store_e_initial_per_period: description: >- whether each investment period opens on the initial energy instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise - dims: [store] + dims: [scenario, store] dtype: bool Store_opens_late: description: >- @@ -781,48 +798,49 @@ parameters: dtype: int Store_marginal_cost: description: cost of one unit of power delivered - dims: [snapshot, store] + dims: [scenario, snapshot, store] Store_marginal_cost_quadratic: description: cost of the square of the net power delivered, so charging costs as much as delivering - dims: [snapshot, store] + dims: [scenario, snapshot, store] Store_marginal_cost_storage: description: cost of one unit of energy held over one snapshot - dims: [snapshot, store] + dims: [scenario, snapshot, store] Store_e_set: description: a given energy schedule; a store without one has no row here - dims: [snapshot, store] + dims: [scenario, snapshot, store] Store_p_set: description: a given schedule of power delivered; a store without one has no row here - dims: [snapshot, store] + dims: [scenario, snapshot, store] Line_s_nom: description: nominal apparent power - dims: [line] + dims: [scenario, line] Line_s_nom_extendable: description: whether the nominal apparent power is a decision dims: [line] dtype: bool Line_s_max_pu: description: most flow either way, per unit of nominal apparent power - dims: [snapshot, line] + dims: [scenario, snapshot, line] Line_s_nom_min: description: least nominal apparent power an extendable line may be built at - dims: [line] + dims: [scenario, line] Line_s_nom_max: description: most nominal apparent power an extendable line may be built at - dims: [line] + dims: [scenario, line] Line_capital_cost: description: cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [line] + dims: [scenario, line] Line_s_nom_set: description: a given nominal apparent power for an extendable line; one without a value has no row here - dims: [line] + dims: [scenario, line] Line_s_set: description: a given flow schedule; a line without one has no row here - dims: [snapshot, line] + dims: [scenario, snapshot, line] Line_cycle_weight: description: >- the line's series impedance, signed by its orientation in the cycle — - the cycle basis, data prep; a line in no cycle has no row + the cycle basis, data prep; a line in no cycle has no row. PyPSA builds + the cycle basis from the first scenario only (`networks.py:1354-1361`) dims: [line, cycle] Line_BODF: description: >- @@ -841,48 +859,49 @@ parameters: dtype: bool Line_loss_max: description: the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep - dims: [snapshot, line] + dims: [scenario, snapshot, line] Line_loss_slope: description: >- the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep - dims: [snapshot, line, segment] + dims: [scenario, snapshot, line, segment] Line_loss_offset: description: >- where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep - dims: [snapshot, line, segment] + dims: [scenario, snapshot, line, segment] Transformer_s_nom: description: nominal apparent power - dims: [transformer] + dims: [scenario, transformer] Transformer_s_nom_extendable: description: whether the nominal apparent power is a decision dims: [transformer] dtype: bool Transformer_s_max_pu: description: most flow either way, per unit of nominal apparent power - dims: [snapshot, transformer] + dims: [scenario, snapshot, transformer] Transformer_s_nom_min: description: least nominal apparent power an extendable transformer may be built at - dims: [transformer] + dims: [scenario, transformer] Transformer_s_nom_max: description: most nominal apparent power an extendable transformer may be built at - dims: [transformer] + dims: [scenario, transformer] Transformer_capital_cost: description: cost of one unit of nominal apparent power — PyPSA's `capital_cost`, periodized as an annuity in data prep - dims: [transformer] + dims: [scenario, transformer] Transformer_s_nom_set: description: a given nominal apparent power for an extendable transformer; one without a value has no row here - dims: [transformer] + dims: [scenario, transformer] Transformer_s_set: description: a given flow schedule; a transformer without one has no row here - dims: [snapshot, transformer] + dims: [scenario, snapshot, transformer] Transformer_cycle_weight: description: >- the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle - basis, data prep; a transformer in no cycle has no row + basis, data prep; a transformer in no cycle has no row. From the first + scenario only, as a line's dims: [transformer, cycle] Transformer_BODF: description: >- @@ -902,7 +921,9 @@ parameters: description: >- whether a transformer's phase shift is a decision — PyPSA's `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a - fixed shift carried by `phase_shift` + fixed shift carried by `phase_shift`. The shift parameters carry no + scenario: only a cycle row reads them, and PyPSA fails on a transformer in + a cycle on a network with scenarios (`constraints.py:1654`) dims: [transformer] dtype: bool Transformer_phase_shift_min: @@ -927,18 +948,18 @@ parameters: the loss at a transformer's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, its `r_pu_eff` the resistance over the given `s_nom` times the tap ratio, data prep - dims: [snapshot, transformer] + dims: [scenario, snapshot, transformer] Transformer_loss_slope: description: >- the slope of a cut to a transformer's loss curve — a tangent's `2 * r_pu_eff * p_k`, a secant's `r_pu_eff * (p_k + p_k+1)`, as a line's, over the transformer's own `r_pu_eff` and rating, data prep - dims: [snapshot, transformer, segment] + dims: [scenario, snapshot, transformer, segment] Transformer_loss_offset: description: >- where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep - dims: [snapshot, transformer, segment] + dims: [scenario, snapshot, transformer, segment] GlobalConstraint_type: description: >- which formula the row takes — `primary_energy`, `operational_limit`, @@ -964,47 +985,51 @@ parameters: dims: [scenario, global_constraint] GlobalConstraint_counts_snapshot: description: >- - whether a row counts a snapshot — PyPSA's `investment_period`: every + whether a row counts a snapshot in a scenario — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:377`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:375`) - dims: [global_constraint, snapshot] + dims: [scenario, global_constraint, snapshot] dtype: bool Generator_primary_energy_weight: description: >- the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency, data prep; a generator of an unweighted carrier has no row - dims: [global_constraint, generator] + dims: [scenario, global_constraint, generator] StorageUnit_primary_energy_weight: description: the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row - dims: [global_constraint, storage_unit] + dims: [scenario, global_constraint, storage_unit] Store_primary_energy_weight: description: the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row - dims: [global_constraint, store] + dims: [scenario, global_constraint, store] Generator_operational_limit_weight: description: one where the generator is in the row's set — data prep; one outside it has no row - dims: [global_constraint, generator] + dims: [scenario, global_constraint, generator] StorageUnit_operational_limit_weight: description: one where the storage unit is in the row's set — data prep; one outside it has no row - dims: [global_constraint, storage_unit] + dims: [scenario, global_constraint, storage_unit] Store_operational_limit_weight: description: one where the store is in the row's set — data prep; one outside it has no row - dims: [global_constraint, store] + dims: [scenario, global_constraint, store] Line_volume_weight: description: >- - the line's length where its carrier is in the row's set — data prep; a + the line's length where its carrier is in the row's set, the first + scenario's length as PyPSA reads it (`global_constraints.py:835-836`) — + data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row - dims: [global_constraint, line] + dims: [scenario, global_constraint, line] Link_volume_weight: description: >- - the link's length where its carrier is in the row's set — data prep; a + the link's length where its carrier is in the row's set, the first + scenario's length as PyPSA reads it (`global_constraints.py:835-836`) — + data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row - dims: [global_constraint, link] + dims: [scenario, global_constraint, link] Line_expansion_cost_weight: description: >- the line's capital cost where its carrier is in the row's set, times @@ -1012,7 +1037,7 @@ parameters: no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row - dims: [global_constraint, line] + dims: [scenario, global_constraint, line] Link_expansion_cost_weight: description: >- the link's capital cost where its carrier is in the row's set, times @@ -1020,7 +1045,7 @@ parameters: no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row - dims: [global_constraint, link] + dims: [scenario, global_constraint, link] Generator_tech_capacity_weight: description: >- one where the generator is in the row's carrier-and-bus set — data @@ -1155,25 +1180,28 @@ relations: values: transformer Generator_maintenance_cover: description: >- - the snapshots a maintenance event covers, by the snapshot it starts in — - the start and the snapshots after it, until their generator weightings - reach `maintenance_duration`, data prep; no rows for a generator that is + the snapshots a maintenance event covers in a scenario, by the snapshot it + starts in — the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a generator that is not maintainable - key: { generator: generator, start: snapshot, covered: snapshot } + key: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } Link_maintenance_cover: description: >- - the snapshots a maintenance event covers, by the snapshot it starts in — - the start and the snapshots after it, until their generator weightings - reach `maintenance_duration`, data prep; no rows for a link that is + the snapshots a maintenance event covers in a scenario, by the snapshot it + starts in — the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a link that is not maintainable - key: { link: link, start: snapshot, covered: snapshot } + key: { scenario: scenario, link: link, start: snapshot, covered: snapshot } Process_maintenance_cover: description: >- - the snapshots a maintenance event covers, by the snapshot it starts in — - the start and the snapshots after it, until their generator weightings - reach `maintenance_duration`, data prep; no rows for a process that is + the snapshots a maintenance event covers in a scenario, by the snapshot it + starts in — the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a process that is not maintainable - key: { process: process, start: snapshot, covered: snapshot } + key: { scenario: scenario, process: process, start: snapshot, covered: snapshot } variables: Generator_p: @@ -1554,7 +1582,7 @@ expressions: otherwise: shift(Generator_p, along=snapshot, offset=1) Generator_p_nom_effective: description: the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise - dims: [generator] + dims: [scenario, generator] cases: extendable: { when: Generator_p_nom_extendable, expression: Generator_p_nom_ext } otherwise: Generator_p_nom @@ -1562,7 +1590,7 @@ expressions: description: >- the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } otherwise: 1 @@ -1570,7 +1598,7 @@ expressions: description: >- the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } otherwise: 1 @@ -1578,7 +1606,7 @@ expressions: description: >- the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_start_up, expression: Generator_ramp_limit_start_up } otherwise: 1 @@ -1586,7 +1614,7 @@ expressions: description: >- the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none - dims: [generator] + dims: [scenario, generator] cases: given: { when: Generator_ramp_limit_shut_down, expression: Generator_ramp_limit_shut_down } otherwise: 1 @@ -1594,7 +1622,7 @@ expressions: description: >- the build a committed unit's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise - dims: [generator] + dims: [scenario, generator] cases: modular_build: { when: Generator_p_nom_extendable AND Generator_p_nom_mod > 0, expression: Generator_p_nom_mod } otherwise: Generator_p_nom @@ -1628,7 +1656,7 @@ expressions: otherwise: Generator_ramp_down_rate * Generator_p_nom_effective Link_p_nom_effective: description: the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise - dims: [link] + dims: [scenario, link] cases: extendable: { when: Link_p_nom_extendable, expression: Link_p_nom_ext } otherwise: Link_p_nom @@ -1653,7 +1681,7 @@ expressions: description: >- the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_up, expression: Link_ramp_limit_up } otherwise: 1 @@ -1661,7 +1689,7 @@ expressions: description: >- the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_down, expression: Link_ramp_limit_down } otherwise: 1 @@ -1669,7 +1697,7 @@ expressions: description: >- the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_start_up, expression: Link_ramp_limit_start_up } otherwise: 1 @@ -1677,7 +1705,7 @@ expressions: description: >- the shut-down ramp a link's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none - dims: [link] + dims: [scenario, link] cases: given: { when: Link_ramp_limit_shut_down, expression: Link_ramp_limit_shut_down } otherwise: 1 @@ -1685,7 +1713,7 @@ expressions: description: >- the build a committed link's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise - dims: [link] + dims: [scenario, link] cases: modular_build: { when: Link_p_nom_extendable AND Link_p_nom_mod > 0, expression: Link_p_nom_mod } otherwise: Link_p_nom @@ -1719,7 +1747,7 @@ expressions: otherwise: Link_ramp_down_rate * Link_p_nom_effective Process_p_nom_effective: description: the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise - dims: [process] + dims: [scenario, process] cases: extendable: { when: Process_p_nom_extendable, expression: Process_p_nom_ext } otherwise: Process_p_nom @@ -1744,7 +1772,7 @@ expressions: description: >- the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_up, expression: Process_ramp_limit_up } otherwise: 1 @@ -1752,7 +1780,7 @@ expressions: description: >- the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_down, expression: Process_ramp_limit_down } otherwise: 1 @@ -1760,7 +1788,7 @@ expressions: description: >- the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_start_up, expression: Process_ramp_limit_start_up } otherwise: 1 @@ -1768,7 +1796,7 @@ expressions: description: >- the shut-down ramp a process's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none - dims: [process] + dims: [scenario, process] cases: given: { when: Process_ramp_limit_shut_down, expression: Process_ramp_limit_shut_down } otherwise: 1 @@ -1776,7 +1804,7 @@ expressions: description: >- the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise - dims: [process] + dims: [scenario, process] cases: modular_build: { when: Process_p_nom_extendable AND Process_p_nom_mod > 0, expression: Process_p_nom_mod } otherwise: Process_p_nom @@ -1907,7 +1935,7 @@ expressions: what one unit of power at a snapshot counts for in a row — the generator weighting times the years of the snapshot's period, where the row counts the snapshot, and nothing where it does not - dims: [global_constraint, snapshot] + dims: [scenario, global_constraint, snapshot] cases: counted: when: GlobalConstraint_counts_snapshot @@ -1915,7 +1943,7 @@ expressions: otherwise: 0 GlobalConstraint_snapshot_closes: description: one at the last snapshot a row counts, and zero elsewhere - dims: [global_constraint, snapshot] + dims: [scenario, global_constraint, snapshot] cases: last_counted: when: GlobalConstraint_counts_snapshot AND NOT shift(GlobalConstraint_counts_snapshot, along=snapshot, offset=-1) @@ -1927,7 +1955,7 @@ expressions: closing level — the years of the period at the last snapshot of each counted period where the unit reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere - dims: [global_constraint, snapshot, storage_unit] + dims: [scenario, global_constraint, snapshot, storage_unit] cases: per_period: when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 @@ -1942,7 +1970,7 @@ expressions: closing level — the years of the period at the last snapshot of each counted period where the store reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere - dims: [global_constraint, snapshot, store] + dims: [scenario, global_constraint, snapshot, store] cases: per_period: when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1 @@ -2093,18 +2121,18 @@ constraints: where: Generator_p_nom_extendable AND not Generator_committable AND Generator_active expression: Generator_p <= Generator_p_max_pu * (Generator_p_nom_ext - Generator_maintenance_pu * Generator_maintenance_capacity) Generator_ext_p_nom_lower: - description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [generator] + description: "`Generator-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, generator] where: Generator_p_nom_extendable expression: Generator_p_nom_ext >= Generator_p_nom_min Generator_ext_p_nom_upper: - description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [generator] + description: "`Generator-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, generator] where: Generator_p_nom_extendable AND Generator_p_nom_max expression: Generator_p_nom_ext <= Generator_p_nom_max Generator_p_nom_set: description: "`Generator-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [generator] + dims: [scenario, generator] where: Generator_p_nom_extendable AND Generator_p_nom_set expression: Generator_p_nom_ext == Generator_p_nom_set Generator_e_sum_min: @@ -2128,18 +2156,18 @@ constraints: where: Link_p_nom_extendable AND not Link_committable AND Link_active expression: Link_p <= Link_p_max_pu * (Link_p_nom_ext - Link_maintenance_pu * Link_maintenance_capacity) Link_ext_p_nom_lower: - description: "`Link-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [link] + description: "`Link-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, link] where: Link_p_nom_extendable expression: Link_p_nom_ext >= Link_p_nom_min Link_ext_p_nom_upper: - description: "`Link-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [link] + description: "`Link-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, link] where: Link_p_nom_extendable AND Link_p_nom_max expression: Link_p_nom_ext <= Link_p_nom_max Link_p_nom_set: description: "`Link-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [link] + dims: [scenario, link] where: Link_p_nom_extendable AND Link_p_nom_set expression: Link_p_nom_ext == Link_p_nom_set Process_fix_p_lower: @@ -2163,18 +2191,18 @@ constraints: where: Process_p_nom_extendable AND not Process_committable AND Process_active expression: Process_p <= Process_p_max_pu * (Process_p_nom_ext - Process_maintenance_pu * Process_maintenance_capacity) Process_ext_p_nom_lower: - description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [process] + description: "`Process-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, process] where: Process_p_nom_extendable expression: Process_p_nom_ext >= Process_p_nom_min Process_ext_p_nom_upper: - description: "`Process-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [process] + description: "`Process-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, process] where: Process_p_nom_extendable AND Process_p_nom_max expression: Process_p_nom_ext <= Process_p_nom_max Process_p_nom_set: description: "`Process-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [process] + dims: [scenario, process] where: Process_p_nom_extendable AND Process_p_nom_set expression: Process_p_nom_ext == Process_p_nom_set StorageUnit_fix_p_dispatch_lower: @@ -3040,18 +3068,18 @@ constraints: where: Line_s_nom_extendable AND Line_active expression: Line_s + Line_loss <= Line_s_max_pu * Line_s_nom_ext Line_ext_s_nom_lower: - description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor" - dims: [line] + description: "`Line-ext-s_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, line] where: Line_s_nom_extendable expression: Line_s_nom_ext >= Line_s_nom_min Line_ext_s_nom_upper: - description: "`Line-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [line] + description: "`Line-ext-s_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, line] where: Line_s_nom_extendable AND Line_s_nom_max expression: Line_s_nom_ext <= Line_s_nom_max Line_s_nom_set: description: "`Line-s_nom_set` — the chosen build pinned, wherever a value is given" - dims: [line] + dims: [scenario, line] where: Line_s_nom_extendable AND Line_s_nom_set expression: Line_s_nom_ext == Line_s_nom_set Line_s_set: @@ -3101,18 +3129,18 @@ constraints: where: Transformer_s_nom_extendable AND Transformer_active expression: Transformer_s + Transformer_loss <= Transformer_s_max_pu * Transformer_s_nom_ext Transformer_ext_s_nom_lower: - description: "`Transformer-ext-s_nom-lower` — the chosen build is at least its floor" - dims: [transformer] + description: "`Transformer-ext-s_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, transformer] where: Transformer_s_nom_extendable expression: Transformer_s_nom_ext >= Transformer_s_nom_min Transformer_ext_s_nom_upper: - description: "`Transformer-ext-s_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [transformer] + description: "`Transformer-ext-s_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, transformer] where: Transformer_s_nom_extendable AND Transformer_s_nom_max expression: Transformer_s_nom_ext <= Transformer_s_nom_max Transformer_s_nom_set: description: "`Transformer-s_nom_set` — the chosen build pinned, wherever a value is given" - dims: [transformer] + dims: [scenario, transformer] where: Transformer_s_nom_extendable AND Transformer_s_nom_set expression: Transformer_s_nom_ext == Transformer_s_nom_set Transformer_s_set: @@ -3357,18 +3385,18 @@ constraints: where: StorageUnit_p_nom_extendable AND StorageUnit_active expression: StorageUnit_state_of_charge <= StorageUnit_max_hours * StorageUnit_p_nom_ext StorageUnit_ext_p_nom_lower: - description: "`StorageUnit-ext-p_nom-lower` — the chosen build is at least its floor" - dims: [storage_unit] + description: "`StorageUnit-ext-p_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, storage_unit] where: StorageUnit_p_nom_extendable expression: StorageUnit_p_nom_ext >= StorageUnit_p_nom_min StorageUnit_ext_p_nom_upper: - description: "`StorageUnit-ext-p_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [storage_unit] + description: "`StorageUnit-ext-p_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, storage_unit] where: StorageUnit_p_nom_extendable AND StorageUnit_p_nom_max expression: StorageUnit_p_nom_ext <= StorageUnit_p_nom_max StorageUnit_p_nom_set: description: "`StorageUnit-p_nom_set` — the chosen build pinned, wherever a value is given" - dims: [storage_unit] + dims: [scenario, storage_unit] where: StorageUnit_p_nom_extendable AND StorageUnit_p_nom_set expression: StorageUnit_p_nom_ext == StorageUnit_p_nom_set StorageUnit_energy_balance: @@ -3405,18 +3433,18 @@ constraints: where: Store_e_nom_extendable AND Store_active expression: Store_e <= Store_e_max_pu * Store_e_nom_ext Store_ext_e_nom_lower: - description: "`Store-ext-e_nom-lower` — the chosen build is at least its floor" - dims: [store] + description: "`Store-ext-e_nom-lower` — the chosen build is at least its floor in every scenario" + dims: [scenario, store] where: Store_e_nom_extendable expression: Store_e_nom_ext >= Store_e_nom_min Store_ext_e_nom_upper: - description: "`Store-ext-e_nom-upper` — the chosen build is at most its cap; a cap of infinity is no row" - dims: [store] + description: "`Store-ext-e_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row" + dims: [scenario, store] where: Store_e_nom_extendable AND Store_e_nom_max expression: Store_e_nom_ext <= Store_e_nom_max Store_e_nom_set: description: "`Store-e_nom_set` — the chosen build pinned, wherever a value is given" - dims: [store] + dims: [scenario, store] where: Store_e_nom_extendable AND Store_e_nom_set expression: Store_e_nom_ext == Store_e_nom_set Store_energy_balance: @@ -3856,14 +3884,16 @@ assumptions: objective: sense: minimize - description: capacity once per active period, operation in expectation over the scenarios, and a share of it at the tail + description: >- + capacity once per active period at its expected cost over the scenarios, operation in + expectation over the scenarios, and a share of it at the tail expression: >- - sum(Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) - + sum(Link_p_nom_ext * Link_capital_cost * Link_capital_weight) - + sum(StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) - + sum(Store_e_nom_ext * Store_capital_cost * Store_capital_weight) - + sum(Line_s_nom_ext * Line_capital_cost * Line_capital_weight) - + sum(Process_p_nom_ext * Process_capital_cost * Process_capital_weight) - + sum(Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + sum(scenario_weight * Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) + + sum(scenario_weight * Link_p_nom_ext * Link_capital_cost * Link_capital_weight) + + sum(scenario_weight * StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) + + sum(scenario_weight * Store_e_nom_ext * Store_capital_cost * Store_capital_weight) + + sum(scenario_weight * Line_s_nom_ext * Line_capital_cost * Line_capital_weight) + + sum(scenario_weight * Process_p_nom_ext * Process_capital_cost * Process_capital_weight) + + sum(scenario_weight * Transformer_s_nom_ext * Transformer_capital_cost * Transformer_capital_weight) + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + CVaR_omega * CVaR diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index a68d6c36..d9c66993 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1592,5 +1592,44 @@ "Link-fix-p-lower": 8, "Link-fix-p-upper": 8 } + }, + "rung_41_scenario_operational_data": { + "columns": { + "Generator-p": 24, + "Link-p": 16 + }, + "global_constraints": {}, + "objective": 17964.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 16, + "Generator-fix-p-lower": 24, + "Generator-fix-p-upper": 24, + "Link-fix-p-lower": 16, + "Link-fix-p-upper": 16 + } + }, + "rung_42_scenario_first_stage": { + "columns": { + "Generator-p": 24, + "Generator-p_nom": 1, + "Link-p": 8 + }, + "global_constraints": {}, + "objective": 5049.999999999999, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 16, + "Generator-ext-p-lower": 8, + "Generator-ext-p-upper": 8, + "Generator-ext-p_nom-lower": 2, + "Generator-ext-p_nom-upper": 2, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8 + } } } diff --git a/examples/references/pypsa/rung_41_scenario_operational_data.py b/examples/references/pypsa/rung_41_scenario_operational_data.py new file mode 100644 index 00000000..890bd560 --- /dev/null +++ b/examples/references/pypsa/rung_41_scenario_operational_data.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 41: a unit's cost and a link's efficiency differ by scenario.""" + +from __future__ import annotations + +import spine + +#: each scenario's own value, per component and attribute +PER_SCENARIO = { + ('calm', 'gas41'): {'marginal_cost': 20}, + ('stormy', 'gas41'): {'marginal_cost': 80}, + ('calm', 'wire41'): {'efficiency': 0.9}, + ('stormy', 'wire41'): {'efficiency': 0.6}, +} + + +def build(): + """The spine over two futures, with a gas unit that costs more and a link that delivers less in the stormy one.""" + n = spine.build() + n.add('Generator', 'gas41', bus='south', p_nom=100, marginal_cost=20) + n.add('Link', 'wire41', bus0='north', bus1='south', p_nom=40, efficiency=0.9) + n.add('Load', 'port41', bus='south', p_set=60) + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + for (scenario, name), values in PER_SCENARIO.items(): + component = n.c.generators if name == 'gas41' else n.c.links + for column, value in values.items(): + component.static.loc[(scenario, name), column] = value + return n diff --git a/examples/references/pypsa/rung_42_scenario_first_stage.py b/examples/references/pypsa/rung_42_scenario_first_stage.py new file mode 100644 index 00000000..73efdecc --- /dev/null +++ b/examples/references/pypsa/rung_42_scenario_first_stage.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 42: an extendable unit's capital cost and build cap differ by scenario.""" + +from __future__ import annotations + +import spine + +#: each scenario's own value, per attribute of the extendable unit +PER_SCENARIO = { + 'calm': {'capital_cost': 20, 'p_nom_max': 100}, + 'stormy': {'capital_cost': 60, 'p_nom_max': 30}, +} + + +def build(): + """The spine over two futures, with an extendable wind unit that costs more and may be built less in the stormy one.""" + n = spine.build() + n.add('Generator', 'wind42', bus='south', p_nom_extendable=True, p_nom_max=100, marginal_cost=1, capital_cost=20) + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + for scenario, values in PER_SCENARIO.items(): + for column, value in values.items(): + n.c.generators.static.loc[(scenario, 'wind42'), column] = value + return n diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index c91d5b3d..b8d0161d 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -75,17 +75,26 @@ def test_a_standard_constraint_changes_frame_only_by_a_leading_scenario(): assert not wrong, f'a standard row may gain only a leading scenario; these differ: {wrong}' -#: the standard parameters PyPSA reads per scenario -SCENARIO_PARAMETERS = {'Load_p_set', 'GlobalConstraint_sense', 'GlobalConstraint_constant'} - - -def test_only_the_scenario_parameters_carry_scenario_among_standard_parameters(): +def test_a_standard_parameter_changes_frame_only_by_a_leading_scenario(): wrong = { name: _dims(ALL.parameters[name]) for name, dims in STANDARD['parameters'].items() - if _dims(ALL.parameters[name]) != (['scenario', *dims] if name in SCENARIO_PARAMETERS else dims) + if _dims(ALL.parameters[name]) not in (dims, ['scenario', *dims]) } - assert not wrong, f'only {sorted(SCENARIO_PARAMETERS)} gain scenario; these differ: {wrong}' + assert not wrong, f'a standard parameter may gain only a leading scenario; these differ: {wrong}' + + +#: PyPSA refuses a difference across scenarios in these attributes and in what is derived from them +INVARIANT_SUFFIXES = ('_extendable', '_committable', '_nom_mod', '_active', '_capital_weight', '_type') + + +def test_what_pypsa_holds_equal_across_scenarios_spans_no_scenario(): + spanning = sorted( + name + for name, block in ALL.parameters.items() + if name.endswith(INVARIANT_SUFFIXES) and 'scenario' in _dims(block) + ) + assert not spanning, f'PyPSA refuses these to differ by scenario, so they carry no scenario: {spanning}' def test_the_extra_axes_and_rows_are_declared(): From 15c2f43c59a5941e9c06ec79a6f7c0b9db3850ce Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 07:24:44 +0200 Subject: [PATCH 32/46] docs(pypsa): a component's sign turns its term in the bus balance around --- docs/examples/pypsa.md | 77 +++++++++++++++++++++-- examples/pypsa.yaml | 35 +++++++++-- examples/references/pypsa/references.json | 32 ++++++++++ examples/references/pypsa/rung_43_sign.py | 22 +++++++ examples/symbols/pypsa.yaml | 4 ++ 5 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 examples/references/pypsa/rung_43_sign.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index ea99fdb6..9d9c1f82 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -85,6 +85,7 @@ def build(): | [`Link-fix-p-lower`](#link-fix-p-lower) | done | | | [`Link-fix-p-upper`](#link-fix-p-upper) | done | | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | a loaded bus with nothing attached: PyPSA refuses, see X2 | +| [`Bus-nodal_balance`](#bus-nodal_balance) with a component `sign` | done | rung 43 | | `Bus-meshed-*-nodal_balance` | out | the same balance rows, dealt into linopy containers by how many component columns name a bus — `meshed_thresholds`, an `n.optimize()` keyword defaulting to `[30, 100, 400]`. Same rows, same duals, another name; a modeler whose engine wants the split states it, the file does not (#123) | | [`marginal_cost`](#objective) | done | | | [`marginal_cost_quadratic`](#objective) | done | rungs 10 and 36, below; Generator and Link `p`, Process `p`, StorageUnit `p_dispatch` only, and Store net `p` | @@ -3578,6 +3579,65 @@ def build(): +### Rung 43 — a component's sign + +`n.optimize()` with a generator, a load, a storage unit and a store whose +`sign` is not PyPSA's default. PyPSA multiplies each of their terms in +`Bus-nodal_balance` by that `sign`: a generator's `p`, a storage unit's +`p_dispatch` and `p_store`, a store's `p` (`constraints.py:1428-1429`), and a +load's `p_set` on the constant side (`constraints.py:1538`). It reads `sign` +nowhere else in the model. The default is `1` for a generator, a storage unit +and a store, and `-1` for a load. PyPSA refuses a `sign` that differs by +scenario (`consistency.py:1187`), so the file states `Generator_sign`, +`Load_sign`, `StorageUnit_sign` and `Store_sign` without `scenario`. A plain run +feeds PyPSA's defaults, and the row collapses to the standard one. + +The rung adds a unit that draws `20` from its bus at a cost of `-50`, a storage +unit that opens full and a full store, each with a `sign` of `-1`, and a load of +`10` with a `sign` of `1`, which feeds its bus. Each sign binds. With the default sign on the +unit, PyPSA solves to `-5652.78`; on the load, to `3925.0`; on the storage unit, +to `1187.5`; on the store, to `925.0`; on all four, to `-5255.56`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Bus-nodal_balance`](#bus-nodal_balance) with a component `sign` | done | `Generator_sign`, `StorageUnit_sign` and `Store_sign` times each term, and `Load_sign` times the load, negated | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `2125.0`, 80 rows. + +
+The network, as PyPSA code + +`rung_43_sign.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 43: a component's `sign` turns its term in the bus balance around.""" + +from __future__ import annotations + +import spine + +#: the sign each component enters the bus balance with, against PyPSA's default +SIGNS = {'generators': ('flex43', -1), 'loads': ('feed43', 1), 'storage_units': ('su43', -1), 'stores': ('e43', -1)} + + +def build(): + """The spine with a unit that draws power, a load that feeds it, and a storage unit and a store drawn the other way round.""" + n = spine.build() + n.add('Generator', 'flex43', bus='south', p_nom=20, marginal_cost=-50, sign=-1) + n.add('Load', 'feed43', bus='north', p_set=10, sign=1) + n.add('StorageUnit', 'su43', bus='south', p_nom=10, max_hours=2, state_of_charge_initial=20, sign=-1) + n.add('Store', 'e43', bus='north', e_nom=30, e_initial=30, sign=-1) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3646,6 +3706,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | | $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | | $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | +| $`\mathrm{sgn}`$ | `Generator_sign` over $`\mathcal{G}`$ — the sign output enters its bus's balance with — PyPSA's `sign`, `1` unless given, `-1` for a unit that draws power. PyPSA refuses one that differs by scenario (`consistency.py:1187`) | | $`\mathrm{com}`$ | `Generator_committable` over $`\mathcal{G}`$ — whether output is gated by an on/off status decision | | $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit | | $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit | @@ -3736,6 +3797,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\Xi \times \mathcal{J}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | +| $`\mathrm{sgn}^{\mathrm{load}}`$ | `Load_sign` over $`\mathcal{D}`$ — the sign a load's demand enters its bus's balance with — PyPSA's `sign`, `-1` unless given, `1` for a load that feeds its bus. PyPSA refuses one that differs by scenario (`consistency.py:1187`) | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | | $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor | @@ -3792,6 +3854,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{T}^{h}`$ | `StorageUnit_max_hours` over $`\Xi \times \mathcal{S}`$ — energy capacity, as hours of dispatch at nominal power | | $`\eta^{-}`$ | `StorageUnit_efficiency_store` over $`\Xi \times \mathcal{S}`$ — share of the power drawn from the bus that becomes charge | | $`\eta^{+}`$ | `StorageUnit_efficiency_dispatch` over $`\Xi \times \mathcal{S}`$ — share of the charge drawn down that reaches the bus | +| $`\mathrm{sgn}^{h}`$ | `StorageUnit_sign` over $`\mathcal{S}`$ — the sign net dispatch enters its bus's balance with — PyPSA's `sign`, `1` unless given. PyPSA refuses one that differs by scenario (`consistency.py:1187`) | | $`\rho`$ | `StorageUnit_retention` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — share of charge kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep | | $`\mathrm{inflow}`$ | `StorageUnit_inflow` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — energy arriving per hour, a river into a reservoir | | $`\mathrm{soc}^{0}`$ | `StorageUnit_state_of_charge_initial` over $`\Xi \times \mathcal{S}`$ — charge held before the first snapshot | @@ -3812,6 +3875,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{ext}^{e}`$ | `Store_e_nom_extendable` over $`\mathcal{V}`$ — whether the nominal energy capacity is a decision | | $`\underline{\mathrm{e}}`$ | `Store_e_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — least energy held, per unit of nominal capacity — negative for a store that may go short | | $`\overline{\mathrm{e}}`$ | `Store_e_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — most energy held, per unit of nominal capacity | +| $`\mathrm{sgn}^{q}`$ | `Store_sign` over $`\mathcal{V}`$ — the sign the power a store delivers enters its bus's balance with — PyPSA's `sign`, `1` unless given. PyPSA refuses one that differs by scenario (`consistency.py:1187`) | | $`\rho^{e}`$ | `Store_retention` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — share of energy kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep | | $`\mathrm{e}^{0}`$ | `Store_e_initial` over $`\Xi \times \mathcal{V}`$ — energy held before the first snapshot | | $`\mathrm{cyc}^{e}`$ | `Store_e_cyclic` over $`\Xi \times \mathcal{V}`$ — whether the horizon closes on itself instead of opening on the initial energy | @@ -7962,14 +8026,15 @@ Bus_nodal_balance: process port drawing or delivering at its own rate and each passive branch carrying its flow, meets the load there, less half of every incident line's and transformer's loss — PyPSA dissipates a branch's loss half at - either end. + either end. Each generator, storage unit, store and load term enters + with its component's `sign` (`constraints.py:1428-1429`, `:1538`). A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - + sum(StorageUnit_p_dispatch - StorageUnit_p_store, by=StorageUnit_bus, over=storage_unit, into=bus) - + sum(Store_p, by=Store_bus, over=store, into=bus) + sum(Generator_sign * Generator_p, by=Generator_bus, over=generator, into=bus) + + sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), by=StorageUnit_bus, over=storage_unit, into=bus) + + sum(Store_sign * Store_p, by=Store_bus, over=store, into=bus) - sum(Link_p, by=Link_bus0, over=link, into=bus) + sum(Link_output_arrival, by=Link_output_bus, over=link_output, into=bus) + sum(Process_output_arrival, by=Process_output_bus, over=process_output, into=bus) @@ -7981,11 +8046,11 @@ Bus_nodal_balance: + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus0, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus1, over=transformer, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) + == -sum(Load_sign * Load_p_set, by=Load_bus, over=load, into=bus) ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{\xi,t,d} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) = -\left( \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{sgn}^{\mathrm{load}}_{d} \cdot \mathrm{load}_{\xi,t,d} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} ``` ### `Carrier-growth_limit` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index d1cd6321..3577a30a 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -95,6 +95,12 @@ parameters: Generator_marginal_cost_quadratic: description: cost of the square of one unit of output dims: [scenario, snapshot, generator] + Generator_sign: + description: >- + the sign output enters its bus's balance with — PyPSA's `sign`, `1` + unless given, `-1` for a unit that draws power. PyPSA refuses one that + differs by scenario (`consistency.py:1187`) + dims: [generator] Generator_committable: description: whether output is gated by an on/off status decision dims: [generator] @@ -494,6 +500,12 @@ parameters: Load_p_set: description: demand dims: [scenario, snapshot, load] + Load_sign: + description: >- + the sign a load's demand enters its bus's balance with — PyPSA's + `sign`, `-1` unless given, `1` for a load that feeds its bus. PyPSA + refuses one that differs by scenario (`consistency.py:1187`) + dims: [load] scenario_weight: description: PyPSA's `scenario_weightings.weight` — the probability of a future dims: [scenario] @@ -678,6 +690,12 @@ parameters: StorageUnit_efficiency_dispatch: description: share of the charge drawn down that reaches the bus dims: [scenario, storage_unit] + StorageUnit_sign: + description: >- + the sign net dispatch enters its bus's balance with — PyPSA's `sign`, + `1` unless given. PyPSA refuses one that differs by scenario + (`consistency.py:1187`) + dims: [storage_unit] StorageUnit_retention: description: share of charge kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep dims: [scenario, snapshot, storage_unit] @@ -757,6 +775,12 @@ parameters: Store_e_max_pu: description: most energy held, per unit of nominal capacity dims: [scenario, snapshot, store] + Store_sign: + description: >- + the sign the power a store delivers enters its bus's balance with — + PyPSA's `sign`, `1` unless given. PyPSA refuses one that differs by + scenario (`consistency.py:1187`) + dims: [store] Store_retention: description: share of energy kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep dims: [scenario, snapshot, store] @@ -3583,14 +3607,15 @@ constraints: process port drawing or delivering at its own rate and each passive branch carrying its flow, meets the load there, less half of every incident line's and transformer's loss — PyPSA dissipates a branch's loss half at - either end. + either end. Each generator, storage unit, store and load term enters + with its component's `sign` (`constraints.py:1428-1429`, `:1538`). A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) - + sum(StorageUnit_p_dispatch - StorageUnit_p_store, by=StorageUnit_bus, over=storage_unit, into=bus) - + sum(Store_p, by=Store_bus, over=store, into=bus) + sum(Generator_sign * Generator_p, by=Generator_bus, over=generator, into=bus) + + sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), by=StorageUnit_bus, over=storage_unit, into=bus) + + sum(Store_sign * Store_p, by=Store_bus, over=store, into=bus) - sum(Link_p, by=Link_bus0, over=link, into=bus) + sum(Link_output_arrival, by=Link_output_bus, over=link_output, into=bus) + sum(Process_output_arrival, by=Process_output_bus, over=process_output, into=bus) @@ -3602,7 +3627,7 @@ constraints: + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus0, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus1, over=transformer, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) + == -sum(Load_sign * Load_p_set, by=Load_bus, over=load, into=bus) Carrier_growth_limit: description: >- `Carrier-growth_limit` — what a carrier adds across its extendable components in a period, diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index d9c66993..31b0d2e2 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1631,5 +1631,37 @@ "Link-fix-p-lower": 8, "Link-fix-p-upper": 8 } + }, + "rung_43_sign": { + "columns": { + "Generator-p": 12, + "Link-p": 4, + "StorageUnit-p_dispatch": 4, + "StorageUnit-p_store": 4, + "StorageUnit-state_of_charge": 4, + "Store-e": 4, + "Store-p": 4 + }, + "global_constraints": {}, + "objective": 2125.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-fix-p-lower": 12, + "Generator-fix-p-upper": 12, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4, + "StorageUnit-energy_balance": 4, + "StorageUnit-fix-p_dispatch-lower": 4, + "StorageUnit-fix-p_dispatch-upper": 4, + "StorageUnit-fix-p_store-lower": 4, + "StorageUnit-fix-p_store-upper": 4, + "StorageUnit-fix-state_of_charge-lower": 4, + "StorageUnit-fix-state_of_charge-upper": 4, + "Store-energy_balance": 4, + "Store-fix-e-lower": 4, + "Store-fix-e-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_43_sign.py b/examples/references/pypsa/rung_43_sign.py new file mode 100644 index 00000000..3c86108a --- /dev/null +++ b/examples/references/pypsa/rung_43_sign.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 43: a component's `sign` turns its term in the bus balance around.""" + +from __future__ import annotations + +import spine + +#: the sign each component enters the bus balance with, against PyPSA's default +SIGNS = {'generators': ('flex43', -1), 'loads': ('feed43', 1), 'storage_units': ('su43', -1), 'stores': ('e43', -1)} + + +def build(): + """The spine with a unit that draws power, a load that feeds it, and a storage unit and a store drawn the other way round.""" + n = spine.build() + n.add('Generator', 'flex43', bus='south', p_nom=20, marginal_cost=-50, sign=-1) + n.add('Load', 'feed43', bus='north', p_set=10, sign=1) + n.add('StorageUnit', 'su43', bus='south', p_nom=10, max_hours=2, state_of_charge_initial=20, sign=-1) + n.add('Store', 'e43', bus='north', e_nom=30, e_initial=30, sign=-1) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index f6789c47..4ed2b4fc 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -32,6 +32,7 @@ names: Generator_p_max_pu: '\overline{\mathrm{p}}' Generator_marginal_cost: '\mathrm{c}' Generator_marginal_cost_quadratic: '\mathrm{c}^{(2)}' + Generator_sign: '\mathrm{sgn}' Generator_committable: '\mathrm{com}' Generator_ramp_limit_up: '\mathrm{ru}' Generator_ramp_limit_down: '\mathrm{rd}' @@ -103,6 +104,7 @@ names: Link_big_m: '\mathrm{M}^{f}' Link_p_min_pu_nonneg: '\mathrm{nonneg}^{f}' Load_p_set: '\mathrm{load}' + Load_sign: '\mathrm{sgn}^{\mathrm{load}}' Generator_p_set: '\mathrm{p}^{\mathrm{set}}' Link_p_set: '\mathrm{f}^{\mathrm{set}}' snapshot_weightings_stores: '\mathrm{w}^{\mathrm{sto}}' @@ -252,6 +254,7 @@ names: StorageUnit_max_hours: '\mathrm{T}^{h}' StorageUnit_efficiency_store: '\eta^{-}' StorageUnit_efficiency_dispatch: '\eta^{+}' + StorageUnit_sign: '\mathrm{sgn}^{h}' StorageUnit_retention: '\rho' StorageUnit_inflow: '\mathrm{inflow}' StorageUnit_state_of_charge_initial: '\mathrm{soc}^{0}' @@ -275,6 +278,7 @@ names: Store_e_nom_extendable: '\mathrm{ext}^{e}' Store_e_min_pu: '\underline{\mathrm{e}}' Store_e_max_pu: '\overline{\mathrm{e}}' + Store_sign: '\mathrm{sgn}^{q}' Store_retention: '\rho^{e}' Store_e_initial: '\mathrm{e}^{0}' Store_energy_carried_in: '\overleftarrow{e}' From 49c9e850db1f125115d9ffdd6f3f691812728ab3 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 07:31:27 +0200 Subject: [PATCH 33/46] docs(pypsa): the linearized commitment file keeps a unit down, ramps at the full build where a limit is missing, and schedules maintenance --- docs/examples/pypsa_linearized_uc.md | 430 +++++++++++++++++- examples/pypsa.yaml | 18 +- examples/pypsa_linearized_uc.yaml | 197 +++++++- examples/references/pypsa/references.json | 39 ++ .../pypsa/rung_44_linearized_commitment.py | 55 +++ examples/symbols/pypsa_linearized_uc.yaml | 14 + 6 files changed, 712 insertions(+), 41 deletions(-) create mode 100644 examples/references/pypsa/rung_44_linearized_commitment.py diff --git a/docs/examples/pypsa_linearized_uc.md b/docs/examples/pypsa_linearized_uc.md index 6ef90674..634322e5 100644 --- a/docs/examples/pypsa_linearized_uc.md +++ b/docs/examples/pypsa_linearized_uc.md @@ -5,8 +5,8 @@ SPDX-License-Identifier: CC-BY-4.0 # PyPSA, the relaxed commitment -Rung 12 of [PyPSA in one file](pypsa.md): `n.optimize(linearized_unit_commitment=True)`, stated on rungs 1 and 7 in a -file of its own. Its network is the spine plus the script's own additions. +Rungs 12 and 44 of [PyPSA in one file](pypsa.md): `n.optimize(linearized_unit_commitment=True)`, stated on rungs 1 and 7 +in a file of its own. Each network is the spine plus the script's own additions. ## Rung 12 — linearized unit commitment @@ -88,6 +88,112 @@ def build(): +## Rung 44 — the integer file's commitment rows, relaxed + +`n.optimize(linearized_unit_commitment=True)` builds the same commitment rows as +the integer run, except the four tightening rows: the keyword only relaxes the +status, start and stop to shares in [0, 1] (`variables.py:81-88`) and adds the +tightening rows (`constraints.py:650`). So this file states each row that +[PyPSA in one file](pypsa.md) states for a fixed committable generator: + +- A unit still serving the down time it brought in stays off + (`constraints.py:622-628`). +- A ramp row stands where a unit has a ramp limit or only a start-up or + shut-down ramp, and a missing limit reads as the full build + (`constraints.py:1046-1055`). The earlier file built no row for a start-up + ramp alone, and read a missing start-up or shut-down ramp as `0`, which kept + a unit that came in off from starting. +- A maintainable unit is taken off for its events. The maintenance start stays + a binary under the keyword (`variables.py:234-259`), and the product of + status and maintenance enters the commitment rows through the `maint-status` + rows (`constraints.py:425-458`). A maintainable unit that is not committable + loses the same share of its fixed rows (`constraints.py:135-145`). + +The rung adds five cheap units under a swinging load. A start costs more than a +stop, so PyPSA does not tighten them and the rung isolates these rows. Each +binds: PyPSA solves to `7400.0`. Without the brought-in down time it solves to +`6540.0`; without the start-up ramp, to `7164.0`; with the missing start-up and +shut-down ramps read as `0`, to `8060.0`; with the committable unit not +maintainable, to `7310.0`; with the fixed unit not maintainable, to `7100.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Generator-com-status-min_down_time_must_stay_up`](#generator-com-status-min_down_time_must_stay_up) | done | | +| [`Generator-p-ramp_limit_up`](#generator-p-ramp_limit_up), [`-down`](#generator-p-ramp_limit_down) for a start-up or shut-down ramp alone, and a missing limit | done | `Generator_ramp_up_rate` and the other three rates read a missing limit as `1` | +| [`Generator-maint-*`](#generator-maint-event-count), [`Generator-maint-status-*`](#generator-maint-status-le-status) | done | fixed units only, as the file states no extendable build | +| [`Generator-com-p-lower`](#generator-com-p-lower), [`-upper`](#generator-com-p-upper), [`Generator-fix-p-lower`](#generator-fix-p-lower), [`-upper`](#generator-fix-p-upper) in maintenance | done | | +| [`Generator-com-p-before`](#generator-com-p-before), [`-current`](#generator-com-p-current), [`-partly-start-up`](#generator-com-partly-start-up), [`-partly-shut-down`](#generator-com-partly-shut-down) with a missing ramp limit | out | PyPSA reads a missing limit as the full build (`constraints.py:311-316`); the file reads it as `0` | +| a ramp row for a unit that is not committable | out | PyPSA builds it with the status fixed at `1` (`constraints.py:1073-1079`); the file states ramp rows for committable units only | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `7400.0`, 191 rows. + +
+The network, as PyPSA code + +`rung_44_linearized_commitment.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 44: linearized commitment with the rows the integer file has — a unit that must stay down, ramps read at the full build where a limit is missing, and maintenance.""" + +from __future__ import annotations + +import spine + +MODEL = 'pypsa_linearized_uc.yaml' +OPTIMIZE = {'linearized_unit_commitment': True} + +#: a start costs more than a stop, so PyPSA does not tighten these units and the rung isolates the rows it adds +UNTIGHTENED = {'committable': True, 'up_time_before': 0, 'start_up_cost': 1} + + +def build(): + """The spine plus five cheap units on a north bus with a swinging load: each carries one of the rows under review.""" + n = spine.build() + n.add( + 'Generator', + 'down44', + bus='north', + p_nom=40, + marginal_cost=2, + min_down_time=3, + down_time_before=1, + **UNTIGHTENED, + ) + n.add('Generator', 'pulse44', bus='north', p_nom=40, marginal_cost=3, ramp_limit_start_up=0.4, **UNTIGHTENED) + n.add( + 'Generator', + 'ramp44', + bus='north', + p_nom=40, + marginal_cost=4, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + **UNTIGHTENED, + ) + n.add( + 'Generator', + 'maint44', + bus='north', + p_nom=40, + p_min_pu=0.3, + marginal_cost=5, + maintainable=True, + maintenance_duration=2, + **UNTIGHTENED, + ) + n.add('Generator', 'fixed44', bus='north', p_nom=20, marginal_cost=1, maintainable=True, maintenance_duration=1) + n.add('Load', 'swing44', bus='north', p_set=[40, 120, 160, 60]) + return n +``` + +
+ + ## The file @@ -97,9 +203,9 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state | Symbol | Meaning | |---|---| -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{Generator\_maintenance\_cover} \subseteq \mathcal{G} \times \mathcal{T} \times \mathcal{T}`$ — dispatch periods | | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Generator\_maintenance\_cover} \subseteq \mathcal{G} \times \mathcal{T} \times \mathcal{T}`$ — generating units, each on one bus | | $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | | $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | @@ -109,6 +215,7 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state | Symbol | Meaning | |---|---| | $`\mathrm{w}`$ | `snapshot_weightings_objective` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost | +| $`\mathrm{w}^{\mathrm{gen}}`$ | `snapshot_weightings_generators` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.generators` — hours a snapshot stands for in an energy total | | $`\mathrm{p}^{\mathrm{nom}}`$ | `Generator_p_nom` over $`\mathcal{G}`$ — nominal power | | $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | | $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | @@ -128,10 +235,16 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state | $`\mathrm{DT}`$ | `Generator_min_down_time` over $`\mathcal{G}`$ — least snapshots a unit stays off once stopped | | $`\mathrm{u}^{0}`$ | `Generator_status_initial` over $`\mathcal{G}`$ — one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | | $`\mathrm{hold}`$ | `Generator_must_stay_up` over $`\mathcal{T} \times \mathcal{G}`$ — true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | +| $`\mathrm{rest}`$ | `Generator_must_stay_down` over $`\mathcal{T} \times \mathcal{G}`$ — true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\mathcal{G}`$ — cost of one start | | $`\mathrm{c}^{\mathrm{dn}}`$ | `Generator_shut_down_cost` over $`\mathcal{G}`$ — cost of one stop | | $`\mathrm{c}^{\mathrm{on}}`$ | `Generator_stand_by_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one snapshot spent on | | $`\mathrm{tight}`$ | `Generator_partly_tightened` over $`\mathcal{G}`$ — whether the four tightening rows below apply — PyPSA adds them only where a unit's start-up and shut-down costs are equal; two parameters cannot be compared in a `where`, so the equality is data prep | +| $`\mathrm{mnt}`$ | `Generator_maintainable` over $`\mathcal{G}`$ — whether a generator must be taken off for maintenance within the horizon | +| $`\gamma`$ | `Generator_maintenance_pu` over $`\mathcal{G}`$ — the share of the build a maintenance event takes off | +| $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\mathcal{G}`$ — how many maintenance events the horizon holds | +| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\mathcal{G}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | #### Variables @@ -142,6 +255,9 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state | $`u`$ | `Generator_status` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-status` — how much of a committable unit is on, a share in \[0, 1\] rather than a binary: the relaxation `linearized_unit_commitment` solves | | $`\mathit{up}`$ | `Generator_start_up` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot | | $`\mathit{dn}`$ | `Generator_shut_down` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot | +| $`\mu`$ | `Generator_maintenance` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance` — whether a maintainable generator is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{\mathrm{up}}`$ | `Generator_maintenance_start` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_start` — whether a maintenance event starts in this snapshot — a binary, which the keyword does not relax | +| $`\mu^{u}`$ | `Generator_maintenance_status` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | #### Definitions @@ -149,6 +265,10 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state |---|---| | $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | +| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{\mathrm{up}}`$ | `Generator_start_up_rate` over $`\mathcal{G}`$ — the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | +| $`\widetilde{\mathrm{rd}}^{\mathrm{dn}}`$ | `Generator_shut_down_rate` over $`\mathcal{G}`$ — the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | $`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, not the order labels sort in — counted from $`0`$. The index itself stays the coordinate, so $`t`$ compares against labels and $`\mathrm{pos}(t)`$ against positions. @@ -179,11 +299,11 @@ Generator_fix_p_lower: description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" dims: [snapshot, generator] where: not Generator_committable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) ``` ```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{com}_{g} +p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( 1 - \gamma_{g} \cdot \mu_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{com}_{g} ``` ### `Generator-fix-p-upper` @@ -195,11 +315,11 @@ Generator_fix_p_upper: description: "`Generator-fix-p-upper` — a generator outputs at most what is available" dims: [snapshot, generator] where: not Generator_committable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) ``` ```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{com}_{g} +p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( 1 - \gamma_{g} \cdot \mu_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{com}_{g} ``` ### `Link-fix-p-lower` @@ -263,11 +383,11 @@ Generator_com_p_lower: description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing" dims: [snapshot, generator] where: Generator_committable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * Generator_status + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) ``` ```math -p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} +p_{t,g} \ge \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{t,g} - \gamma_{g} \cdot \mu^{u}_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} ``` ### `Generator-com-p-upper` @@ -279,11 +399,11 @@ Generator_com_p_upper: description: "`Generator-com-p-upper` — a committed unit outputs at most what is available; off, at most nothing" dims: [snapshot, generator] where: Generator_committable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * Generator_status + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) ``` ```math -p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} +p_{t,g} \le \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{t,g} - \gamma_{g} \cdot \mu^{u}_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} ``` ### `Generator-com-transition-start-up` @@ -343,7 +463,10 @@ Generator_com_up_time: ```yaml Generator_com_down_time: - description: "`Generator-com-down-time` — a unit stopped within its own minimum down time is still off" + description: >- + `Generator-com-down-time` — a unit stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries dims: [snapshot, generator] where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 expression: sum_back(Generator_shut_down, along=snapshot, window=Generator_min_down_time) <= 1 - Generator_status @@ -369,6 +492,24 @@ Generator_com_status_must_stay_up: u_{t,g} = 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{hold}_{t,g} ``` +### `Generator-com-status-min_down_time_must_stay_up` + +`Generator_com_status_must_stay_down` + +```yaml +Generator_com_status_must_stay_down: + description: >- + `Generator-com-status-min_down_time_must_stay_up` — a unit still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [snapshot, generator] + where: Generator_committable AND Generator_must_stay_down + expression: Generator_status == 0 +``` + +```math +u_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{rest}_{t,g} +``` + ### `Generator-p-ramp_limit_up` `Generator_p_ramp_limit_up` @@ -383,17 +524,17 @@ Generator_p_ramp_limit_up: first snapshot dims: [snapshot, generator] where: >- - Generator_committable AND Generator_ramp_limit_up + Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND (position(snapshot) > 0 OR Generator_status_initial == 0) expression: >- Generator_p - Generator_previous_p <= - Generator_ramp_limit_up * Generator_p_nom * Generator_previous_status - + Generator_ramp_limit_start_up * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom * Generator_previous_status + + Generator_start_up_rate * Generator_p_nom * (Generator_status - Generator_previous_status) ``` ```math -p_{t,g} - \overleftarrow{p}_{t,g} \le \mathrm{ru}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{t,g} + \mathrm{ru}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{t,g} - \overleftarrow{u}_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ru}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +p_{t,g} - \overleftarrow{p}_{t,g} \le \widetilde{\mathrm{ru}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{t,g} - \overleftarrow{u}_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) ``` ### `Generator-p-ramp_limit_down` @@ -410,17 +551,17 @@ Generator_p_ramp_limit_down: first snapshot dims: [snapshot, generator] where: >- - Generator_committable AND Generator_ramp_limit_down + Generator_committable AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Generator_status_initial == 0) expression: >- Generator_previous_p - Generator_p <= - Generator_ramp_limit_down * Generator_p_nom * Generator_status - + Generator_ramp_limit_shut_down * Generator_p_nom + Generator_ramp_down_rate * Generator_p_nom * Generator_status + + Generator_shut_down_rate * Generator_p_nom * (Generator_previous_status - Generator_status) ``` ```math -\overleftarrow{p}_{t,g} - p_{t,g} \le \mathrm{rd}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} + \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{t,g} - u_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{rd}_{g} \text{ is defined} \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +\overleftarrow{p}_{t,g} - p_{t,g} \le \widetilde{\mathrm{rd}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{t,g} - u_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) ``` ### `Generator-status-p-fixed-upper` @@ -471,6 +612,105 @@ Generator_shut_down_p_fixed_upper: \mathit{dn}_{t,g} \le 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} ``` +### `Generator-maint-event-count` + +`Generator_maint_event_count` + +```yaml +Generator_maint_event_count: + description: "`Generator-maint-event-count` — a maintainable generator holds its number of maintenance events over the horizon" + dims: [generator] + where: Generator_maintainable + expression: sum(Generator_maintenance_start, over=snapshot) == Generator_maintenance_events +``` + +```math +\sum_{t \in \mathcal{T}} \mu^{\mathrm{up}}_{t,g} = \mathrm{n}^{\mathrm{mnt}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator-maint-window` + +`Generator_maint_window` + +```yaml +Generator_maint_window: + description: >- + `Generator-maint-window` — a generator is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [snapshot, generator] + where: Generator_maintainable + expression: Generator_maintenance == sum(Generator_maintenance_start, by=Generator_maintenance_cover, over=start, into=covered) +``` + +```math +\mu_{t,g} = \sum_{t' \in \mathcal{T} \,:\, \left( g,\ t',\ t \right) \in \mathrm{Generator\_maintenance\_cover}} \mu^{\mathrm{up}}_{t',g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator-maint-start-horizon` + +`Generator_maint_start_horizon` + +```yaml +Generator_maint_start_horizon: + description: "`Generator-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_maintenance_start_blocked + expression: Generator_maintenance_start == 0 +``` + +```math +\mu^{\mathrm{up}}_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{blk}_{t,g} +``` + +### `Generator-maint-status-le-status` + +`Generator_maint_status_le_status` + +```yaml +Generator_maint_status_le_status: + description: "`Generator-maint-status-le-status` — the status in maintenance is at most the status" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_committable + expression: Generator_maintenance_status <= Generator_status +``` + +```math +\mu^{u}_{t,g} \le u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} +``` + +### `Generator-maint-status-le-maint` + +`Generator_maint_status_le_maint` + +```yaml +Generator_maint_status_le_maint: + description: "`Generator-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_committable + expression: Generator_maintenance_status <= Generator_maintenance +``` + +```math +\mu^{u}_{t,g} \le \mu_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} +``` + +### `Generator-maint-status-lb` + +`Generator_maint_status_lb` + +```yaml +Generator_maint_status_lb: + description: "`Generator-maint-status-lb` — on and in maintenance, the status in maintenance is one" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_committable + expression: Generator_maintenance_status >= Generator_status + Generator_maintenance - 1 +``` + +```math +\mu^{u}_{t,g} \ge u_{t,g} + \mu_{t,g} - 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} +``` + ### `Generator-com-p-before` `Generator_com_p_before` @@ -590,6 +830,74 @@ Generator_previous_p: \overleftarrow{p}_{t,g} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ p_{t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` +### `Generator_ramp_up_rate` + +```yaml +Generator_ramp_up_rate: + description: >- + the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}_{g} = \begin{cases} \mathrm{ru}_{g} & \text{if } \mathrm{ru}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + +### `Generator_ramp_down_rate` + +```yaml +Generator_ramp_down_rate: + description: >- + the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}_{g} = \begin{cases} \mathrm{rd}_{g} & \text{if } \mathrm{rd}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + +### `Generator_start_up_rate` + +```yaml +Generator_start_up_rate: + description: >- + the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_start_up, expression: Generator_ramp_limit_start_up } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} = \begin{cases} \mathrm{ru}^{\mathrm{up}}_{g} & \text{if } \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + +### `Generator_shut_down_rate` + +```yaml +Generator_shut_down_rate: + description: >- + the shut-down ramp a unit's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_shut_down, expression: Generator_ramp_limit_shut_down } + otherwise: 1 +``` + +```math +\widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} = \begin{cases} \mathrm{rd}^{\mathrm{dn}}_{g} & \text{if } \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +``` + #### Variable domains **`Generator_p`** @@ -621,6 +929,86 @@ u_{t,g} \ge 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \math ```math \mathit{dn}_{t,g} \ge 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} ``` + +**`Generator_maintenance`** + +```math +0 \le \mu_{t,g} \le 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +**`Generator_maintenance_start`** + +```math +\mu^{\mathrm{up}}_{t,g} \in \{0, 1\} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +**`Generator_maintenance_status`** + +```math +\mu^{u}_{t,g} \ge 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} +``` + +### `Generator_maintenance_events_positive` + +```yaml +Generator_maintenance_events_positive: + holds: "Generator_maintenance_events > 0" + where: "Generator_maintainable" + description: >- + a maintainable generator with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) +``` + +```math +\mathrm{n}^{\mathrm{mnt}}_{g} > 0 \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator_maintenance_duration_positive` + +```yaml +Generator_maintenance_duration_positive: + holds: "Generator_maintenance_duration > 0" + where: "Generator_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) +``` + +```math +\tau^{\mathrm{mnt}}_{g} > 0 \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator_maintenance_duration_fits_the_horizon` + +```yaml +Generator_maintenance_duration_fits_the_horizon: + holds: "Generator_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) +``` + +```math +\tau^{\mathrm{mnt}}_{g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` + +### `Generator_maintenance_events_fit_the_horizon` + +```yaml +Generator_maintenance_events_fit_the_horizon: + holds: "Generator_maintenance_duration * Generator_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) +``` + +```math +\tau^{\mathrm{mnt}}_{g} \cdot \mathrm{n}^{\mathrm{mnt}}_{g} \le \sum_{t \in \mathcal{T}} \mathrm{w}^{\mathrm{gen}}_{t} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} +``` Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 3577a30a..2a361987 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -1204,25 +1204,25 @@ relations: values: transformer Generator_maintenance_cover: description: >- - the snapshots a maintenance event covers in a scenario, by the snapshot it - starts in — the start and the snapshots after it, until their generator - weightings reach that scenario's `maintenance_duration`, data prep; no + the snapshots a maintenance event covers, by the snapshot it starts in — + in a scenario, the start and the snapshots after it, until their + generator weightings reach that scenario's `maintenance_duration`, data prep; no rows for a generator that is not maintainable key: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } Link_maintenance_cover: description: >- - the snapshots a maintenance event covers in a scenario, by the snapshot it - starts in — the start and the snapshots after it, until their generator - weightings reach that scenario's `maintenance_duration`, data prep; no + the snapshots a maintenance event covers, by the snapshot it starts in — + in a scenario, the start and the snapshots after it, until their + generator weightings reach that scenario's `maintenance_duration`, data prep; no rows for a link that is not maintainable key: { scenario: scenario, link: link, start: snapshot, covered: snapshot } Process_maintenance_cover: description: >- - the snapshots a maintenance event covers in a scenario, by the snapshot it - starts in — the start and the snapshots after it, until their generator - weightings reach that scenario's `maintenance_duration`, data prep; no + the snapshots a maintenance event covers, by the snapshot it starts in — + in a scenario, the start and the snapshots after it, until their + generator weightings reach that scenario's `maintenance_duration`, data prep; no rows for a process that is not maintainable key: { scenario: scenario, process: process, start: snapshot, covered: snapshot } diff --git a/examples/pypsa_linearized_uc.yaml b/examples/pypsa_linearized_uc.yaml index 2d09651c..6c11d5c4 100644 --- a/examples/pypsa_linearized_uc.yaml +++ b/examples/pypsa_linearized_uc.yaml @@ -32,6 +32,9 @@ parameters: snapshot_weightings_objective: description: PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost dims: [snapshot] + snapshot_weightings_generators: + description: PyPSA's `snapshot_weightings.generators` — hours a snapshot stands for in an energy total + dims: [snapshot] Generator_p_nom: description: nominal power dims: [generator] @@ -100,6 +103,13 @@ parameters: parameter dims: [snapshot, generator] dtype: bool + Generator_must_stay_down: + description: >- + true while the down time a unit brought into the horizon still binds — + PyPSA's `min_down_time - down_time_before` snapshots, where + `down_time_before > 0`, data prep for the same reason + dims: [snapshot, generator] + dtype: bool Generator_start_up_cost: description: cost of one start dims: [generator] @@ -116,6 +126,33 @@ parameters: compared in a `where`, so the equality is data prep dims: [generator] dtype: bool + Generator_maintainable: + description: whether a generator must be taken off for maintenance within the horizon + dims: [generator] + dtype: bool + Generator_maintenance_pu: + description: the share of the build a maintenance event takes off + dims: [generator] + Generator_maintenance_events: + description: how many maintenance events the horizon holds + dims: [generator] + dtype: int + Generator_maintenance_duration: + description: >- + the hours of generator weightings one maintenance event covers — + PyPSA's `maintenance_duration`; no value where the generator is not + maintainable. No row reads it: data prep turns it into + `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the + assumptions hold it to the horizon + dims: [generator] + Generator_maintenance_start_blocked: + description: >- + true where no maintenance event may start, because the snapshots it + would cover run past the end of the horizon or into one the generator does + not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` + and the generator weightings, data prep + dims: [snapshot, generator] + dtype: bool relations: Generator_bus: @@ -141,6 +178,13 @@ relations: description: the bus a load sits on key: load values: bus + Generator_maintenance_cover: + description: >- + the snapshots a maintenance event covers, by the snapshot it starts in — + the start and the snapshots after it, until their generator weightings + reach `maintenance_duration`, data prep; no rows for a generator that is + not maintainable + key: { generator: generator, start: snapshot, covered: snapshot } variables: Generator_p: @@ -173,6 +217,34 @@ variables: where: Generator_committable bounds: lower: 0 + Generator_maintenance: + description: >- + `Generator-maintenance` — whether a maintainable generator is in maintenance: + continuous, and one exactly where an event covers the snapshot + dims: [snapshot, generator] + where: Generator_maintainable + absence: zero + bounds: + lower: 0 + upper: 1 + Generator_maintenance_start: + description: >- + `Generator-maintenance_start` — whether a maintenance event starts in + this snapshot — a binary, which the keyword does not relax + dims: [snapshot, generator] + where: Generator_maintainable + absence: zero + domain: binary + Generator_maintenance_status: + description: >- + `Generator-maintenance_status` — the status while in maintenance, zero + otherwise: the product the `maint-status` rows linearize, so a unit in + maintenance may also be off + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_committable + absence: zero + bounds: + lower: 0 expressions: Generator_previous_status: @@ -192,18 +264,50 @@ expressions: cases: opening: { when: "position(snapshot) == 0", expression: 0 } otherwise: shift(Generator_p, along=snapshot, offset=1) + Generator_ramp_up_rate: + description: >- + the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the + full build where it has none, since a start-up ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } + otherwise: 1 + Generator_ramp_down_rate: + description: >- + the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or + the full build where it has none, since a shut-down ramp alone builds the row + dims: [generator] + cases: + given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } + otherwise: 1 + Generator_start_up_rate: + description: >- + the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, + or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_start_up, expression: Generator_ramp_limit_start_up } + otherwise: 1 + Generator_shut_down_rate: + description: >- + the shut-down ramp a unit's down row reads — PyPSA's + `ramp_limit_shut_down`, or the full build where it has none + dims: [generator] + cases: + given: { when: Generator_ramp_limit_shut_down, expression: Generator_ramp_limit_shut_down } + otherwise: 1 constraints: Generator_fix_p_lower: description: "`Generator-fix-p-lower` — a generator outputs at least its minimum" dims: [snapshot, generator] where: not Generator_committable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) Generator_fix_p_upper: description: "`Generator-fix-p-upper` — a generator outputs at most what is available" dims: [snapshot, generator] where: not Generator_committable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (1 - Generator_maintenance_pu * Generator_maintenance) Link_fix_p_lower: description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way" dims: [snapshot, link] @@ -227,12 +331,12 @@ constraints: description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing" dims: [snapshot, generator] where: Generator_committable - expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * Generator_status + expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) Generator_com_p_upper: description: "`Generator-com-p-upper` — a committed unit outputs at most what is available; off, at most nothing" dims: [snapshot, generator] where: Generator_committable - expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * Generator_status + expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * (Generator_status - Generator_maintenance_pu * Generator_maintenance_status) Generator_com_transition_start_up: description: "`Generator-com-transition-start-up` — turning on is a start, counted against the state the unit carried into the snapshot" dims: [snapshot, generator] @@ -252,7 +356,10 @@ constraints: where: Generator_committable AND Generator_min_up_time > 0 AND position(snapshot) > 0 expression: sum_back(Generator_start_up, along=snapshot, window=Generator_min_up_time) <= Generator_status Generator_com_down_time: - description: "`Generator-com-down-time` — a unit stopped within its own minimum down time is still off" + description: >- + `Generator-com-down-time` — a unit stopped within its own minimum down + time is still off. The first snapshot's share of the window is the + brought-in down time's, which the must-stay-down mask carries dims: [snapshot, generator] where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0 expression: sum_back(Generator_shut_down, along=snapshot, window=Generator_min_down_time) <= 1 - Generator_status @@ -261,6 +368,13 @@ constraints: dims: [snapshot, generator] where: Generator_committable AND Generator_must_stay_up expression: Generator_status == 1 + Generator_com_status_must_stay_down: + description: >- + `Generator-com-status-min_down_time_must_stay_up` — a unit still serving + the down time it brought in stays off; PyPSA names the row `_must_stay_up` + dims: [snapshot, generator] + where: Generator_committable AND Generator_must_stay_down + expression: Generator_status == 0 Generator_p_ramp_limit_up: description: >- `Generator-p-ramp_limit_up` — a committed unit raises output no faster @@ -270,12 +384,12 @@ constraints: first snapshot dims: [snapshot, generator] where: >- - Generator_committable AND Generator_ramp_limit_up + Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND (position(snapshot) > 0 OR Generator_status_initial == 0) expression: >- Generator_p - Generator_previous_p <= - Generator_ramp_limit_up * Generator_p_nom * Generator_previous_status - + Generator_ramp_limit_start_up * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom * Generator_previous_status + + Generator_start_up_rate * Generator_p_nom * (Generator_status - Generator_previous_status) Generator_p_ramp_limit_down: description: >- @@ -286,12 +400,12 @@ constraints: first snapshot dims: [snapshot, generator] where: >- - Generator_committable AND Generator_ramp_limit_down + Generator_committable AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND (position(snapshot) > 0 OR Generator_status_initial == 0) expression: >- Generator_previous_p - Generator_p <= - Generator_ramp_limit_down * Generator_p_nom * Generator_status - + Generator_ramp_limit_shut_down * Generator_p_nom + Generator_ramp_down_rate * Generator_p_nom * Generator_status + + Generator_shut_down_rate * Generator_p_nom * (Generator_previous_status - Generator_status) Generator_status_p_fixed_upper: description: "`Generator-status-p-fixed-upper` — a status is at most one, an explicit row as PyPSA writes it" @@ -308,6 +422,39 @@ constraints: dims: [snapshot, generator] where: Generator_committable expression: Generator_shut_down <= 1 + Generator_maint_event_count: + description: "`Generator-maint-event-count` — a maintainable generator holds its number of maintenance events over the horizon" + dims: [generator] + where: Generator_maintainable + expression: sum(Generator_maintenance_start, over=snapshot) == Generator_maintenance_events + Generator_maint_window: + description: >- + `Generator-maint-window` — a generator is in maintenance exactly where an event it + started covers the snapshot; two events do not overlap, since the + maintenance status is at most one + dims: [snapshot, generator] + where: Generator_maintainable + expression: Generator_maintenance == sum(Generator_maintenance_start, by=Generator_maintenance_cover, over=start, into=covered) + Generator_maint_start_horizon: + description: "`Generator-maint-start-horizon` — no event starts where it could not run its whole duration" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_maintenance_start_blocked + expression: Generator_maintenance_start == 0 + Generator_maint_status_le_status: + description: "`Generator-maint-status-le-status` — the status in maintenance is at most the status" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_committable + expression: Generator_maintenance_status <= Generator_status + Generator_maint_status_le_maint: + description: "`Generator-maint-status-le-maint` — out of maintenance, the status in maintenance is zero" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_committable + expression: Generator_maintenance_status <= Generator_maintenance + Generator_maint_status_lb: + description: "`Generator-maint-status-lb` — on and in maintenance, the status in maintenance is one" + dims: [snapshot, generator] + where: Generator_maintainable AND Generator_committable + expression: Generator_maintenance_status >= Generator_status + Generator_maintenance - 1 Generator_com_p_before: description: >- `Generator-com-p-before` — the output a unit had entering this snapshot @@ -358,3 +505,31 @@ objective: + sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective) + sum(Generator_start_up * Generator_start_up_cost) + sum(Generator_shut_down * Generator_shut_down_cost) + +assumptions: + Generator_maintenance_events_positive: + holds: "Generator_maintenance_events > 0" + where: "Generator_maintainable" + description: >- + a maintainable generator with no event schedules no maintenance — + PyPSA refuses it (`consistency.py:1516`) + Generator_maintenance_duration_positive: + holds: "Generator_maintenance_duration > 0" + where: "Generator_maintainable" + description: >- + an event that covers no hours is no maintenance window — PyPSA + refuses it (`consistency.py:1506`) + Generator_maintenance_duration_fits_the_horizon: + holds: "Generator_maintenance_duration <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + one event longer than the horizon, in generator weightings, blocks + every start and makes the event count infeasible — PyPSA refuses it + (`consistency.py:1527`) + Generator_maintenance_events_fit_the_horizon: + holds: "Generator_maintenance_duration * Generator_maintenance_events <= sum(snapshot_weightings_generators, over=snapshot)" + where: "Generator_maintainable" + description: >- + the events together longer than the horizon, in generator + weightings, cannot all be scheduled — PyPSA refuses it + (`consistency.py:1539`) diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 31b0d2e2..97342eaa 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1663,5 +1663,44 @@ "Store-fix-e-lower": 4, "Store-fix-e-upper": 4 } + }, + "rung_44_linearized_commitment": { + "columns": { + "Generator-maintenance": 8, + "Generator-maintenance_start": 8, + "Generator-maintenance_status": 4, + "Generator-p": 28, + "Generator-shut_down": 16, + "Generator-start_up": 16, + "Generator-status": 16, + "Link-p": 4 + }, + "global_constraints": {}, + "objective": 7400.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-com-down-time": 3, + "Generator-com-p-lower": 16, + "Generator-com-p-upper": 16, + "Generator-com-status-min_down_time_must_stay_up": 2, + "Generator-com-transition-shut-down": 16, + "Generator-com-transition-start-up": 16, + "Generator-fix-p-lower": 12, + "Generator-fix-p-upper": 12, + "Generator-maint-event-count": 2, + "Generator-maint-status-lb": 4, + "Generator-maint-status-le-maint": 4, + "Generator-maint-status-le-status": 4, + "Generator-maint-window": 8, + "Generator-p-ramp_limit_down": 4, + "Generator-p-ramp_limit_up": 8, + "Generator-shut_down-p-fixed-upper": 16, + "Generator-start_up-p-fixed-upper": 16, + "Generator-status-p-fixed-upper": 16, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_44_linearized_commitment.py b/examples/references/pypsa/rung_44_linearized_commitment.py new file mode 100644 index 00000000..f51b5e57 --- /dev/null +++ b/examples/references/pypsa/rung_44_linearized_commitment.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 44: linearized commitment with the rows the integer file has — a unit that must stay down, ramps read at the full build where a limit is missing, and maintenance.""" + +from __future__ import annotations + +import spine + +MODEL = 'pypsa_linearized_uc.yaml' +OPTIMIZE = {'linearized_unit_commitment': True} + +#: a start costs more than a stop, so PyPSA does not tighten these units and the rung isolates the rows it adds +UNTIGHTENED = {'committable': True, 'up_time_before': 0, 'start_up_cost': 1} + + +def build(): + """The spine plus five cheap units on a north bus with a swinging load: each carries one of the rows under review.""" + n = spine.build() + n.add( + 'Generator', + 'down44', + bus='north', + p_nom=40, + marginal_cost=2, + min_down_time=3, + down_time_before=1, + **UNTIGHTENED, + ) + n.add('Generator', 'pulse44', bus='north', p_nom=40, marginal_cost=3, ramp_limit_start_up=0.4, **UNTIGHTENED) + n.add( + 'Generator', + 'ramp44', + bus='north', + p_nom=40, + marginal_cost=4, + ramp_limit_up=0.5, + ramp_limit_down=0.5, + **UNTIGHTENED, + ) + n.add( + 'Generator', + 'maint44', + bus='north', + p_nom=40, + p_min_pu=0.3, + marginal_cost=5, + maintainable=True, + maintenance_duration=2, + **UNTIGHTENED, + ) + n.add('Generator', 'fixed44', bus='north', p_nom=20, marginal_cost=1, maintainable=True, maintenance_duration=1) + n.add('Load', 'swing44', bus='north', p_set=[40, 120, 160, 60]) + return n diff --git a/examples/symbols/pypsa_linearized_uc.yaml b/examples/symbols/pypsa_linearized_uc.yaml index 3e9c5bce..a6516c3f 100644 --- a/examples/symbols/pypsa_linearized_uc.yaml +++ b/examples/symbols/pypsa_linearized_uc.yaml @@ -43,3 +43,17 @@ names: Link_marginal_cost: '\mathrm{c}^{f}' Load_p_set: '\mathrm{load}' Generator_partly_tightened: '\mathrm{tight}' + snapshot_weightings_generators: '\mathrm{w}^{\mathrm{gen}}' + Generator_must_stay_down: '\mathrm{rest}' + Generator_ramp_up_rate: '\widetilde{\mathrm{ru}}' + Generator_ramp_down_rate: '\widetilde{\mathrm{rd}}' + Generator_start_up_rate: '\widetilde{\mathrm{ru}}^{\mathrm{up}}' + Generator_shut_down_rate: '\widetilde{\mathrm{rd}}^{\mathrm{dn}}' + Generator_maintainable: '\mathrm{mnt}' + Generator_maintenance_pu: '\gamma' + Generator_maintenance_events: '\mathrm{n}^{\mathrm{mnt}}' + Generator_maintenance_duration: '\tau^{\mathrm{mnt}}' + Generator_maintenance_start_blocked: '\mathrm{blk}' + Generator_maintenance: '\mu' + Generator_maintenance_start: '\mu^{\mathrm{up}}' + Generator_maintenance_status: '\mu^{u}' From 19b1992da0ea1d60d5a15a0b52e1e2f2ea318732 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 07:58:55 +0200 Subject: [PATCH 34/46] docs(pypsa): a ramp limit may change over time and lift at a snapshot --- docs/examples/pypsa.md | 180 +++++++++++++----- examples/pypsa.yaml | 36 ++-- examples/references/pypsa/references.json | 26 +++ .../pypsa/rung_45_ramp_per_snapshot.py | 44 +++++ tests/fixtures/pypsa_standard_shape.yaml | 6 + 5 files changed, 226 insertions(+), 66 deletions(-) create mode 100644 examples/references/pypsa/rung_45_ramp_per_snapshot.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 9d9c1f82..828c800d 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -377,6 +377,7 @@ def build(): | PyPSA | status | note | | ------------------------------ | ------ | ---------------------------------------------------------- | | [`{c}-p-ramp_limit_up/down`](#generator-p-ramp_limit_up) | done | the build, the allowance and the output carried in are cased quantities, so fixed, extendable and committed are one block; big-M is rung 8's. A missing limit reads as the full build, and a start-up or shut-down ramp alone builds the row, rung 28 | +| [`{c}-p-ramp_limit_up/down`](#generator-p-ramp_limit_up) with a limit per snapshot | done | rung 45 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `8785.0`, 64 rows. @@ -3638,6 +3639,89 @@ def build(): +### Rung 45 — ramp limits per snapshot + +`n.optimize()` with a generator, a link and a process whose `ramp_limit_up` +and `ramp_limit_down` change over time. PyPSA declares both `static or series` +for all three components, and `ramp_limit_start_up` and `ramp_limit_shut_down` +static. The row between two snapshots reads the limit at the later snapshot +(`constraints.py:1040-1041`, `1109-1110`, `1139-1140`). The "no limit" test is +made per snapshot (`constraints.py:1046-1047`), and a missing value reads as +the full build there (`constraints.py:1052-1055`). So the file states +`{c}_ramp_limit_up` and `{c}_ramp_limit_down` over `snapshot`, and +`{c}_ramp_up_rate` and `{c}_ramp_down_rate` with them. A snapshot without a +value has no row in the table, so the `where:` drops the row there unless a +start-up or shut-down ramp builds it. A plain run feeds the same value at each +snapshot, and the rows collapse to the standard ones. + +The rung adds a steep bus with a load of `90` at the third snapshot. Each unit +may raise its output by `0.1` of its build, and by `0.5` into the third +snapshot. It may lower its output by `0.1`, and without a limit into the last +snapshot. With the constant limit `0.1` in each direction, the backup covers +the peak, and PyPSA solves to `90871.0` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [`{c}-p-ramp_limit_up/down`](#generator-p-ramp_limit_up) with a limit per snapshot | done | `{c}_ramp_limit_up`, `{c}_ramp_limit_down` and their rates span `snapshot`; a snapshot without a value drops the row unless a start-up or shut-down ramp builds it | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `10996.0`, 83 rows. + +
+The network, as PyPSA code + +`rung_45_ramp_per_snapshot.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 45: ramp limits per snapshot — a generator, a link and a process whose ramp limits change over time, and lift at one snapshot.""" + +from __future__ import annotations + +import math + +import spine + + +def build(): + """The spine plus a steep bus served by a generator, a link and a process with ramp limits per snapshot, with a dear backup.""" + n = spine.build() + n.add('Bus', 'steep') + up = [0.1, 0.1, 0.5, 0.1] + down = [0.1, 0.1, 0.1, math.nan] + n.add('Generator', 'steep_gen', bus='steep', p_nom=60, marginal_cost=3, ramp_limit_up=up, ramp_limit_down=down) + n.add( + 'Link', + 'steep_link', + bus0='north', + bus1='steep', + p_nom=40, + marginal_cost=4, + ramp_limit_up=up, + ramp_limit_down=down, + ) + n.add( + 'Process', + 'steep_proc', + bus0='south', + bus1='steep', + rate0=-1.25, + p_nom=40, + marginal_cost=5, + ramp_limit_up=up, + ramp_limit_down=down, + ) + n.add('Generator', 'steep_backup', bus='steep', p_nom=200, marginal_cost=500) + n.add('Load', 'steep_load', bus='steep', p_set=[10, 20, 90, 10]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3708,8 +3792,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | | $`\mathrm{sgn}`$ | `Generator_sign` over $`\mathcal{G}`$ — the sign output enters its bus's balance with — PyPSA's `sign`, `1` unless given, `-1` for a unit that draws power. PyPSA refuses one that differs by scenario (`consistency.py:1187`) | | $`\mathrm{com}`$ | `Generator_committable` over $`\mathcal{G}`$ — whether output is gated by an on/off status decision | -| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | | $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot a unit starts, per unit of nominal power | | $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot before a unit stops, per unit of nominal power | | $`\mathrm{UT}`$ | `Generator_min_up_time` over $`\Xi \times \mathcal{G}`$ — least snapshots a unit stays on once started | @@ -3729,8 +3813,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\Xi \times \mathcal{G}`$ — how many maintenance events the horizon holds | | $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\Xi \times \mathcal{G}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | -| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\Xi \times \mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\Xi \times \mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | | $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\Xi \times \mathcal{L}`$ — nominal power | | $`\mathrm{ext}^{f}`$ | `Link_p_nom_extendable` over $`\mathcal{L}`$ — whether the nominal power is a decision | | $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | @@ -3769,8 +3853,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost. One for every scenario, as the delay | | $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | | $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | -| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\Xi \times \mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | | $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — a given internal power schedule; a process without one has no row here | | $`\underline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_min` over $`\Xi \times \mathcal{J}`$ — least nominal power an extendable process may be built at | | $`\overline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_max` over $`\Xi \times \mathcal{J}`$ — most nominal power an extendable process may be built at | @@ -4001,8 +4085,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | | $`\widetilde{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_effective` over $`\Xi \times \mathcal{G}`$ — the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise | -| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\Xi \times \mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | -| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\Xi \times \mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | | $`\widetilde{\mathrm{ru}}^{\mathrm{up}}`$ | `Generator_start_up_rate` over $`\Xi \times \mathcal{G}`$ — the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | | $`\widetilde{\mathrm{rd}}^{\mathrm{dn}}`$ | `Generator_shut_down_rate` over $`\Xi \times \mathcal{G}`$ — the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | | $`\widehat{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_committed` over $`\Xi \times \mathcal{G}`$ — the build a committed unit's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | @@ -4011,8 +4095,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\Xi \times \mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{f}`$ | `Link_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the commitment state a link carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{f}`$ | `Link_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the flow a link carries into a snapshot — nothing at the start of the horizon, which is why a link that came in running carries no ramp row there | -| $`\widetilde{\mathrm{ru}}^{f}`$ | `Link_ramp_up_rate` over $`\Xi \times \mathcal{L}`$ — the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | -| $`\widetilde{\mathrm{rd}}^{f}`$ | `Link_ramp_down_rate` over $`\Xi \times \mathcal{L}`$ — the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{f}`$ | `Link_ramp_up_rate` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}^{f}`$ | `Link_ramp_down_rate` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | | $`\widetilde{\mathrm{ru}}^{f,\mathrm{up}}`$ | `Link_start_up_rate` over $`\Xi \times \mathcal{L}`$ — the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | | $`\widetilde{\mathrm{rd}}^{f,\mathrm{dn}}`$ | `Link_shut_down_rate` over $`\Xi \times \mathcal{L}`$ — the shut-down ramp a link's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | | $`\widehat{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_committed` over $`\Xi \times \mathcal{L}`$ — the build a committed link's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | @@ -4021,8 +4105,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\Xi \times \mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{z}`$ | `Process_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the commitment state a process carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{z}`$ | `Process_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the internal power a process carries into a snapshot — nothing at the start of the horizon, which is why a process that came in running carries no ramp row there | -| $`\widetilde{\mathrm{ru}}^{z}`$ | `Process_ramp_up_rate` over $`\Xi \times \mathcal{J}`$ — the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | -| $`\widetilde{\mathrm{rd}}^{z}`$ | `Process_ramp_down_rate` over $`\Xi \times \mathcal{J}`$ — the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\widetilde{\mathrm{ru}}^{z}`$ | `Process_ramp_up_rate` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}^{z}`$ | `Process_ramp_down_rate` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | | $`\widetilde{\mathrm{ru}}^{z,\mathrm{up}}`$ | `Process_start_up_rate` over $`\Xi \times \mathcal{J}`$ — the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | | $`\widetilde{\mathrm{rd}}^{z,\mathrm{dn}}`$ | `Process_shut_down_rate` over $`\Xi \times \mathcal{J}`$ — the shut-down ramp a process's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | | $`\widehat{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_committed` over $`\Xi \times \mathcal{J}`$ — the build a committed process's ramp rows are taken against — one module where the build is extendable and modular, the given build otherwise | @@ -4711,7 +4795,7 @@ Generator_p_ramp_limit_up_run_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{\xi,t,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-start-bigM` @@ -4737,7 +4821,7 @@ Generator_p_ramp_limit_up_start_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-run-bigM` @@ -4763,7 +4847,7 @@ Generator_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{\xi,t,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-shut-bigM` @@ -4789,7 +4873,7 @@ Generator_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p_nom_modularity` @@ -5399,7 +5483,7 @@ Link_p_ramp_limit_up_run_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{\xi,t,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_up-start-bigM` @@ -5425,7 +5509,7 @@ Link_p_ramp_limit_up_start_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-run-bigM` @@ -5451,7 +5535,7 @@ Link_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{\xi,t,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-shut-bigM` @@ -5477,7 +5561,7 @@ Link_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p_nom_modularity` @@ -6087,7 +6171,7 @@ Process_p_ramp_limit_up_run_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{\xi,t,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_up-start-bigM` @@ -6113,7 +6197,7 @@ Process_p_ramp_limit_up_start_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-run-bigM` @@ -6139,7 +6223,7 @@ Process_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{\xi,t,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-shut-bigM` @@ -6165,7 +6249,7 @@ Process_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p_nom_modularity` @@ -7200,7 +7284,7 @@ Generator_p_ramp_limit_up: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{\xi,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down` @@ -7225,7 +7309,7 @@ Generator_p_ramp_limit_down: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{\xi,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} ``` ### `Link-p-ramp_limit_up` @@ -7250,7 +7334,7 @@ Link_p_ramp_limit_up: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down` @@ -7275,7 +7359,7 @@ Link_p_ramp_limit_down: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-p-ramp_limit_up` @@ -7300,7 +7384,7 @@ Process_p_ramp_limit_up: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down` @@ -7325,7 +7409,7 @@ Process_p_ramp_limit_down: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `StorageUnit-ext-p_dispatch-lower` @@ -8165,14 +8249,14 @@ Generator_ramp_up_rate: description: >- the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [scenario, generator] + dims: [scenario, snapshot, generator] cases: given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}_{\xi,g} = \begin{cases} \mathrm{ru}_{\xi,g} & \text{if } \mathrm{ru}_{\xi,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} +\widetilde{\mathrm{ru}}_{\xi,t,g} = \begin{cases} \mathrm{ru}_{\xi,t,g} & \text{if } \mathrm{ru}_{\xi,t,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_ramp_down_rate` @@ -8182,14 +8266,14 @@ Generator_ramp_down_rate: description: >- the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [scenario, generator] + dims: [scenario, snapshot, generator] cases: given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}_{\xi,g} = \begin{cases} \mathrm{rd}_{\xi,g} & \text{if } \mathrm{rd}_{\xi,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} +\widetilde{\mathrm{rd}}_{\xi,t,g} = \begin{cases} \mathrm{rd}_{\xi,t,g} & \text{if } \mathrm{rd}_{\xi,t,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_start_up_rate` @@ -8263,7 +8347,7 @@ Generator_ramp_up_allowance: ``` ```math -\Delta^{+}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{ru}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{ru}}_{\xi,g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{+}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{ru}}_{\xi,t,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( u_{\xi,t,g} - \overleftarrow{u}_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{ru}}_{\xi,t,g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_ramp_down_allowance` @@ -8286,7 +8370,7 @@ Generator_ramp_down_allowance: ``` ```math -\Delta^{-}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{rd}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot u_{\xi,t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{rd}}_{\xi,g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\Delta^{-}_{\xi,t,g} = \begin{cases} \widetilde{\mathrm{rd}}_{\xi,t,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot u_{\xi,t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot \widehat{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} \cdot \left( \overleftarrow{u}_{\xi,t,g} - u_{\xi,t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{rd}}_{\xi,t,g} \cdot \widetilde{\mathrm{p}}^{\mathrm{nom}}_{\xi,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Link_p_nom_effective` @@ -8346,14 +8430,14 @@ Link_ramp_up_rate: description: >- the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [scenario, link] + dims: [scenario, snapshot, link] cases: given: { when: Link_ramp_limit_up, expression: Link_ramp_limit_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}^{f}_{\xi,l} = \begin{cases} \mathrm{ru}^{f}_{\xi,l} & \text{if } \mathrm{ru}^{f}_{\xi,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} +\widetilde{\mathrm{ru}}^{f}_{\xi,t,l} = \begin{cases} \mathrm{ru}^{f}_{\xi,t,l} & \text{if } \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Link_ramp_down_rate` @@ -8363,14 +8447,14 @@ Link_ramp_down_rate: description: >- the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [scenario, link] + dims: [scenario, snapshot, link] cases: given: { when: Link_ramp_limit_down, expression: Link_ramp_limit_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}^{f}_{\xi,l} = \begin{cases} \mathrm{rd}^{f}_{\xi,l} & \text{if } \mathrm{rd}^{f}_{\xi,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ l \in \mathcal{L} +\widetilde{\mathrm{rd}}^{f}_{\xi,t,l} = \begin{cases} \mathrm{rd}^{f}_{\xi,t,l} & \text{if } \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Link_start_up_rate` @@ -8444,7 +8528,7 @@ Link_ramp_up_allowance: ``` ```math -\Delta^{f,+}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{ru}}^{f}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} + \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{ru}}^{f}_{\xi,l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\Delta^{f,+}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{ru}}^{f}_{\xi,t,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} + \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( u^{f}_{\xi,t,l} - \overleftarrow{u}^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{ru}}^{f}_{\xi,t,l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Link_ramp_down_allowance` @@ -8467,7 +8551,7 @@ Link_ramp_down_allowance: ``` ```math -\Delta^{f,-}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{rd}}^{f}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot u^{f}_{\xi,t,l} + \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{rd}}^{f}_{\xi,l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\Delta^{f,-}_{\xi,t,l} = \begin{cases} \widetilde{\mathrm{rd}}^{f}_{\xi,t,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot u^{f}_{\xi,t,l} + \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot \widehat{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} \cdot \left( \overleftarrow{u}^{f}_{\xi,t,l} - u^{f}_{\xi,t,l} \right) & \text{if } \mathrm{com}^{f}_{l} \\ \widetilde{\mathrm{rd}}^{f}_{\xi,t,l} \cdot \widetilde{\mathrm{f}}^{\mathrm{nom}}_{\xi,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Process_p_nom_effective` @@ -8527,14 +8611,14 @@ Process_ramp_up_rate: description: >- the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [scenario, process] + dims: [scenario, snapshot, process] cases: given: { when: Process_ramp_limit_up, expression: Process_ramp_limit_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}^{z}_{\xi,j} = \begin{cases} \mathrm{ru}^{z}_{\xi,j} & \text{if } \mathrm{ru}^{z}_{\xi,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} +\widetilde{\mathrm{ru}}^{z}_{\xi,t,j} = \begin{cases} \mathrm{ru}^{z}_{\xi,t,j} & \text{if } \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `Process_ramp_down_rate` @@ -8544,14 +8628,14 @@ Process_ramp_down_rate: description: >- the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [scenario, process] + dims: [scenario, snapshot, process] cases: given: { when: Process_ramp_limit_down, expression: Process_ramp_limit_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}^{z}_{\xi,j} = \begin{cases} \mathrm{rd}^{z}_{\xi,j} & \text{if } \mathrm{rd}^{z}_{\xi,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ j \in \mathcal{J} +\widetilde{\mathrm{rd}}^{z}_{\xi,t,j} = \begin{cases} \mathrm{rd}^{z}_{\xi,t,j} & \text{if } \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `Process_start_up_rate` @@ -8625,7 +8709,7 @@ Process_ramp_up_allowance: ``` ```math -\Delta^{z,+}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{ru}}^{z}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} + \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{ru}}^{z}_{\xi,j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\Delta^{z,+}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{ru}}^{z}_{\xi,t,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} + \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( u^{z}_{\xi,t,j} - \overleftarrow{u}^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{ru}}^{z}_{\xi,t,j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `Process_ramp_down_allowance` @@ -8648,7 +8732,7 @@ Process_ramp_down_allowance: ``` ```math -\Delta^{z,-}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{rd}}^{z}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot u^{z}_{\xi,t,j} + \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{rd}}^{z}_{\xi,j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\Delta^{z,-}_{\xi,t,j} = \begin{cases} \widetilde{\mathrm{rd}}^{z}_{\xi,t,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot u^{z}_{\xi,t,j} + \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot \widehat{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} \cdot \left( \overleftarrow{u}^{z}_{\xi,t,j} - u^{z}_{\xi,t,j} \right) & \text{if } \mathrm{com}^{z}_{j} \\ \widetilde{\mathrm{rd}}^{z}_{\xi,t,j} \cdot \widetilde{\mathrm{z}}^{\mathrm{nom}}_{\xi,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `StorageUnit_charge_carried_in` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 2a361987..dc64f276 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -106,11 +106,11 @@ parameters: dims: [generator] dtype: bool Generator_ramp_limit_up: - description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit - dims: [scenario, generator] + description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [scenario, snapshot, generator] Generator_ramp_limit_down: - description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit - dims: [scenario, generator] + description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [scenario, snapshot, generator] Generator_ramp_limit_start_up: description: most output in the snapshot a unit starts, per unit of nominal power dims: [scenario, generator] @@ -201,11 +201,11 @@ parameters: dims: [scenario, snapshot, generator] dtype: bool Link_ramp_limit_up: - description: most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit - dims: [scenario, link] + description: most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [scenario, snapshot, link] Link_ramp_limit_down: - description: most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit - dims: [scenario, link] + description: most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [scenario, snapshot, link] Link_p_nom: description: nominal power dims: [scenario, link] @@ -384,11 +384,11 @@ parameters: description: cost of the square of one unit of internal power dims: [scenario, snapshot, process] Process_ramp_limit_up: - description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit - dims: [scenario, process] + description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [scenario, snapshot, process] Process_ramp_limit_down: - description: most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit - dims: [scenario, process] + description: most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [scenario, snapshot, process] Process_p_set: description: a given internal power schedule; a process without one has no row here dims: [scenario, snapshot, process] @@ -1614,7 +1614,7 @@ expressions: description: >- the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [scenario, generator] + dims: [scenario, snapshot, generator] cases: given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } otherwise: 1 @@ -1622,7 +1622,7 @@ expressions: description: >- the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [scenario, generator] + dims: [scenario, snapshot, generator] cases: given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } otherwise: 1 @@ -1705,7 +1705,7 @@ expressions: description: >- the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [scenario, link] + dims: [scenario, snapshot, link] cases: given: { when: Link_ramp_limit_up, expression: Link_ramp_limit_up } otherwise: 1 @@ -1713,7 +1713,7 @@ expressions: description: >- the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [scenario, link] + dims: [scenario, snapshot, link] cases: given: { when: Link_ramp_limit_down, expression: Link_ramp_limit_down } otherwise: 1 @@ -1796,7 +1796,7 @@ expressions: description: >- the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [scenario, process] + dims: [scenario, snapshot, process] cases: given: { when: Process_ramp_limit_up, expression: Process_ramp_limit_up } otherwise: 1 @@ -1804,7 +1804,7 @@ expressions: description: >- the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [scenario, process] + dims: [scenario, snapshot, process] cases: given: { when: Process_ramp_limit_down, expression: Process_ramp_limit_down } otherwise: 1 diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 97342eaa..bd284b9d 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1702,5 +1702,31 @@ "Link-fix-p-lower": 4, "Link-fix-p-upper": 4 } + }, + "rung_45_ramp_per_snapshot": { + "columns": { + "Generator-p": 16, + "Link-p": 8, + "Process-p": 4 + }, + "global_constraints": {}, + "objective": 10996.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Generator-p-ramp_limit_down": 2, + "Generator-p-ramp_limit_up": 3, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8, + "Link-p-ramp_limit_down": 2, + "Link-p-ramp_limit_up": 3, + "Process-fix-p-lower": 4, + "Process-fix-p-upper": 4, + "Process-p-ramp_limit_down": 2, + "Process-p-ramp_limit_up": 3 + } } } diff --git a/examples/references/pypsa/rung_45_ramp_per_snapshot.py b/examples/references/pypsa/rung_45_ramp_per_snapshot.py new file mode 100644 index 00000000..dd15877a --- /dev/null +++ b/examples/references/pypsa/rung_45_ramp_per_snapshot.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 45: ramp limits per snapshot — a generator, a link and a process whose ramp limits change over time, and lift at one snapshot.""" + +from __future__ import annotations + +import math + +import spine + + +def build(): + """The spine plus a steep bus served by a generator, a link and a process with ramp limits per snapshot, with a dear backup.""" + n = spine.build() + n.add('Bus', 'steep') + up = [0.1, 0.1, 0.5, 0.1] + down = [0.1, 0.1, 0.1, math.nan] + n.add('Generator', 'steep_gen', bus='steep', p_nom=60, marginal_cost=3, ramp_limit_up=up, ramp_limit_down=down) + n.add( + 'Link', + 'steep_link', + bus0='north', + bus1='steep', + p_nom=40, + marginal_cost=4, + ramp_limit_up=up, + ramp_limit_down=down, + ) + n.add( + 'Process', + 'steep_proc', + bus0='south', + bus1='steep', + rate0=-1.25, + p_nom=40, + marginal_cost=5, + ramp_limit_up=up, + ramp_limit_down=down, + ) + n.add('Generator', 'steep_backup', bus='steep', p_nom=200, marginal_cost=500) + n.add('Load', 'steep_load', bus='steep', p_set=[10, 20, 90, 10]) + return n diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index fd3c36c8..6952a957 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -551,12 +551,14 @@ parameters: - global_constraint - generator Generator_ramp_limit_down: + - snapshot - generator Generator_ramp_limit_shut_down: - generator Generator_ramp_limit_start_up: - generator Generator_ramp_limit_up: + - snapshot - generator Generator_shut_down_cost: - generator @@ -674,12 +676,14 @@ parameters: - snapshot - link Link_ramp_limit_down: + - snapshot - link Link_ramp_limit_shut_down: - link Link_ramp_limit_start_up: - link Link_ramp_limit_up: + - snapshot - link Link_shut_down_cost: - link @@ -753,12 +757,14 @@ parameters: - snapshot - process Process_ramp_limit_down: + - snapshot - process Process_ramp_limit_shut_down: - process Process_ramp_limit_start_up: - process Process_ramp_limit_up: + - snapshot - process Process_rate: - process_output From d5b4e7590a71da900f43ed9f897d29262e8f4c0a Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 08:06:12 +0200 Subject: [PATCH 35/46] docs(pypsa): a unit that came in running ramps from its p_init into the first snapshot --- docs/examples/pypsa.md | 314 ++++++++++++++---- examples/pypsa.yaml | 141 +++++--- examples/references/pypsa/references.json | 50 +++ .../pypsa/rung_46_initial_output.py | 60 ++++ examples/symbols/pypsa.yaml | 3 + 5 files changed, 460 insertions(+), 108 deletions(-) create mode 100644 examples/references/pypsa/rung_46_initial_output.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 828c800d..36e23bc8 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -378,6 +378,7 @@ def build(): | ------------------------------ | ------ | ---------------------------------------------------------- | | [`{c}-p-ramp_limit_up/down`](#generator-p-ramp_limit_up) | done | the build, the allowance and the output carried in are cased quantities, so fixed, extendable and committed are one block; big-M is rung 8's. A missing limit reads as the full build, and a start-up or shut-down ramp alone builds the row, rung 28 | | [`{c}-p-ramp_limit_up/down`](#generator-p-ramp_limit_up) with a limit per snapshot | done | rung 45 | +| [`{c}-p-ramp_limit_*`](#generator-p-ramp_limit_up), `-bigM`, at the first snapshot from `p_init` | done | rung 46 | > ✔ `pypsa 1.3.0` solves this rung's network at objective `8785.0`, 64 rows. @@ -3722,6 +3723,117 @@ def build(): +### Rung 46 — the output brought in + +`n.optimize()` with units that carry `p_init`, the output they brought into the +horizon. PyPSA reads `p_init` only where a unit came in running +(`up_time_before > 0`), and reads zero where it came in off +(`constraints.py:1091-1092`). It builds the ramp rows at the first snapshot +where that value exists (`constraints.py:1094`), so a unit that came in running +without `p_init` has none there, as before. It carries the value into the first +snapshot's rows with the status the unit came in with (`constraints.py:1101-1106`). +This is the same for a Generator, a Link and a Process. It holds for a fixed, +an extendable and a committable build, and for the big-M rows of a committable +extendable build (`constraints.py:937-946`). PyPSA reads `p_init` per scenario. +Under `multi_investment_periods` it reads it only at the horizon's first +snapshot, because no ramp row stands at a later period start +(`constraints.py:1097-1099`). The file states `{c}_p_init`, and the output +carried in at the first snapshot is `{c}_status_initial * {c}_p_init`. The +first-snapshot `where:` reads `{c}_status_initial == 0 OR {c}_p_init`. A +plain run feeds no `p_init`, and the rows collapse to the standard ones. + +PyPSA warns where a committable generator came in off and has a `p_init`, and +ignores the value (`consistency.py:669-680`). The product with +`{c}_status_initial` ignores it too. A unit that is not committable and came in +off is refused: see [Refusals](#refusals). PyPSA sets the first-snapshot mask +without its `active` mask, so a unit that does not stand at the first snapshot +still gets a row there, with no variable in it. The file does not state that +row. + +The rung adds a warm bus with a load of `150`, then `60`, served by six +ramp-limited units: a fixed generator, a link from `p_init = 0`, a dear process +that came in at full output, an extendable generator, a committable generator +and a committable extendable generator. Each `p_init` binds. Without any of +them, PyPSA solves to `9921.0` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [`{c}-p-ramp_limit_*`](#generator-p-ramp_limit_up), [`-bigM`](#generator-p-ramp_limit_up-run-bigm), at the first snapshot | done | `{c}_previous_p` opens on `{c}_status_initial * {c}_p_init`; the row stands where `{c}_status_initial == 0 OR {c}_p_init` | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `101294.125`, 200 rows. + +
+The network, as PyPSA code + +`rung_46_initial_output.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 46: the output brought in — units that came in running with a given `p_init` ramp from it into the first snapshot.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a warm bus served by fixed, extendable and committable units that each carry a `p_init`, with a dear backup.""" + n = spine.build() + n.add('Bus', 'warm') + ramps = {'ramp_limit_up': 0.25, 'ramp_limit_down': 0.25} + n.add('Generator', 'warm_gen', bus='warm', p_nom=60, marginal_cost=3, p_init=10, **ramps) + n.add('Link', 'warm_link', bus0='north', bus1='warm', p_nom=40, marginal_cost=4, p_init=0, **ramps) + n.add( + 'Process', 'warm_proc', bus0='south', bus1='warm', rate0=-1.25, p_nom=40, marginal_cost=600, p_init=40, **ramps + ) + n.add( + 'Generator', + 'warm_ext', + bus='warm', + p_nom_extendable=True, + p_nom_max=40, + capital_cost=5, + marginal_cost=2, + p_init=5, + **ramps, + ) + n.add( + 'Generator', + 'warm_com', + bus='warm', + committable=True, + p_nom=50, + p_min_pu=0.2, + marginal_cost=2.5, + p_init=30, + ramp_limit_start_up=0.4, + ramp_limit_shut_down=0.4, + **ramps, + ) + n.add( + 'Generator', + 'warm_com_ext', + bus='warm', + committable=True, + p_nom_extendable=True, + p_nom_max=40, + capital_cost=5, + marginal_cost=2.2, + p_init=5, + **ramps, + ) + n.add('Generator', 'warm_backup', bus='warm', p_nom=300, marginal_cost=500) + n.add('Load', 'warm_load', bus='warm', p_set=[150, 150, 60, 60]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -3745,6 +3857,7 @@ where it should live — language, data prep, or harness — is one open questio | `NotImplementedError`, `global_constraints.py:66-68` | a `tech_capacity_expansion_limit` row on a network with scenarios | assumed where there is more than one scenario: [`GlobalConstraint_tech_capacity_expansion_limit_without_scenarios`](#globalconstraint_tech_capacity_expansion_limit_without_scenarios). The file cannot tell one scenario from none, which PyPSA also refuses | | | `ConsistencyError`, `consistency.py:1506-1560` | a maintainable component whose `maintenance_duration` or `maintenance_events` is not positive, whose events do not fit the weighted horizon, or that is extendable with `p_nom_max = inf` | assumed: [`Generator_maintenance_events_positive`](#generator_maintenance_events_positive), [`-duration_positive`](#generator_maintenance_duration_positive), [`-duration_fits_the_horizon`](#generator_maintenance_duration_fits_the_horizon), [`-events_fit_the_horizon`](#generator_maintenance_events_fit_the_horizon), [`-build_cap_is_finite`](#generator_maintenance_build_cap_is_finite), and the `Link` and `Process` ones | | | nothing; HiGHS refuses the model, `constraints.py:500-503` | a fixed modular committable maintainable build, whose module count `p_nom_max / p_nom_mod` is infinite | assumed: [`Generator_maintenance_module_count_is_finite`](#generator_maintenance_module_count_is_finite), and the `Link` and `Process` ones | | +| nothing; PyPSA builds the row, `constraints.py:1091-1094`, `1110-1112` | a ramp-limited Generator, Link or Process that is not committable, with `up_time_before = 0` | assumed: [`Generator_came_in_running_unless_committable`](#generator_came_in_running_unless_committable), and the `Link` and `Process` ones. PyPSA caps the unit at zero in the first snapshot, or at its start-up ramp where another unit of the component is committable with a fixed build, and documents `up_time_before` as read only for a committable unit | | Duals and solutions are read back by the harness on the lpspec side: `marginal_price` is the balance dual over `w_objective`, `mu_upper` the @@ -3799,6 +3912,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{UT}`$ | `Generator_min_up_time` over $`\Xi \times \mathcal{G}`$ — least snapshots a unit stays on once started | | $`\mathrm{DT}`$ | `Generator_min_down_time` over $`\Xi \times \mathcal{G}`$ — least snapshots a unit stays off once stopped | | $`\mathrm{u}^{0}`$ | `Generator_status_initial` over $`\Xi \times \mathcal{G}`$ — one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{p}^{0}`$ | `Generator_p_init` over $`\Xi \times \mathcal{G}`$ — the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot | | $`\mathrm{hold}`$ | `Generator_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | | $`\mathrm{rest}`$ | `Generator_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\Xi \times \mathcal{G}`$ — cost of one start | @@ -3830,6 +3944,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{UT}^{f}`$ | `Link_min_up_time` over $`\Xi \times \mathcal{L}`$ — least snapshots a link stays on once started | | $`\mathrm{DT}^{f}`$ | `Link_min_down_time` over $`\Xi \times \mathcal{L}`$ — least snapshots a link stays off once stopped | | $`\mathrm{u}^{f,0}`$ | `Link_status_initial` over $`\Xi \times \mathcal{L}`$ — one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{f}^{0}`$ | `Link_p_init` over $`\Xi \times \mathcal{L}`$ — the flow a link brought into the horizon — PyPSA's `p_init`, read only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot | | $`\mathrm{hold}^{f}`$ | `Link_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true while the up time a link brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | | $`\mathrm{rest}^{f}`$ | `Link_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true while the down time a link brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{f,\mathrm{up}}`$ | `Link_start_up_cost` over $`\Xi \times \mathcal{L}`$ — cost of one start | @@ -3866,6 +3981,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{UT}^{z}`$ | `Process_min_up_time` over $`\Xi \times \mathcal{J}`$ — least snapshots a process stays on once started | | $`\mathrm{DT}^{z}`$ | `Process_min_down_time` over $`\Xi \times \mathcal{J}`$ — least snapshots a process stays off once stopped | | $`\mathrm{u}^{z,0}`$ | `Process_status_initial` over $`\Xi \times \mathcal{J}`$ — one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{z}^{0}`$ | `Process_p_init` over $`\Xi \times \mathcal{J}`$ — the internal power a process brought into the horizon — PyPSA's `p_init`, read only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot | | $`\mathrm{hold}^{z}`$ | `Process_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true while the up time a process brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | | $`\mathrm{rest}^{z}`$ | `Process_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true while the down time a process brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{z,\mathrm{up}}`$ | `Process_start_up_cost` over $`\Xi \times \mathcal{J}`$ — cost of one start | @@ -4083,7 +4199,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | Symbol | Meaning | |---|---| | $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | -| $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | +| $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — at the first, the `p_init` it brought in where it came in running and nothing where it came in off; the previous snapshot's after that | | $`\widetilde{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_effective` over $`\Xi \times \mathcal{G}`$ — the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | | $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | @@ -4094,7 +4210,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_effective` over $`\Xi \times \mathcal{L}`$ — the build a link's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{f}`$ | `Link_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the commitment state a link carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | -| $`\overleftarrow{f}`$ | `Link_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the flow a link carries into a snapshot — nothing at the start of the horizon, which is why a link that came in running carries no ramp row there | +| $`\overleftarrow{f}`$ | `Link_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the flow a link carries into a snapshot — at the first, the `p_init` it brought in where it came in running and nothing where it came in off; the previous snapshot's after that | | $`\widetilde{\mathrm{ru}}^{f}`$ | `Link_ramp_up_rate` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the ramp limit a link's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | | $`\widetilde{\mathrm{rd}}^{f}`$ | `Link_ramp_down_rate` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — the ramp limit a link's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | | $`\widetilde{\mathrm{ru}}^{f,\mathrm{up}}`$ | `Link_start_up_rate` over $`\Xi \times \mathcal{L}`$ — the start-up ramp a link's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | @@ -4104,7 +4220,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\Delta^{f,-}`$ | `Link_ramp_down_allowance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — how far a link may lower flow between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | | $`\widetilde{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_effective` over $`\Xi \times \mathcal{J}`$ — the build a process's limits are taken against — the chosen one where it is extendable, the given one otherwise | | $`\overleftarrow{u}^{z}`$ | `Process_previous_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the commitment state a process carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | -| $`\overleftarrow{z}`$ | `Process_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the internal power a process carries into a snapshot — nothing at the start of the horizon, which is why a process that came in running carries no ramp row there | +| $`\overleftarrow{z}`$ | `Process_previous_p` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the internal power a process carries into a snapshot — at the first, the `p_init` it brought in where it came in running and nothing where it came in off; the previous snapshot's after that | | $`\widetilde{\mathrm{ru}}^{z}`$ | `Process_ramp_up_rate` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the ramp limit a process's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | | $`\widetilde{\mathrm{rd}}^{z}`$ | `Process_ramp_down_rate` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — the ramp limit a process's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | | $`\widetilde{\mathrm{ru}}^{z,\mathrm{up}}`$ | `Process_start_up_rate` over $`\Xi \times \mathcal{J}`$ — the start-up ramp a process's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | @@ -4786,7 +4902,7 @@ Generator_p_ramp_limit_up_run_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_p - Generator_previous_p <= @@ -4795,7 +4911,7 @@ Generator_p_ramp_limit_up_run_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{\xi,t,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}_{\xi,t,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \overleftarrow{u}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_up-start-bigM` @@ -4812,7 +4928,7 @@ Generator_p_ramp_limit_up_start_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_p - Generator_previous_p <= @@ -4821,7 +4937,7 @@ Generator_p_ramp_limit_up_start_big_m: ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \widetilde{\mathrm{ru}}^{\mathrm{up}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{up}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-run-bigM` @@ -4838,7 +4954,7 @@ Generator_p_ramp_limit_down_run_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_previous_p - Generator_p <= @@ -4847,7 +4963,7 @@ Generator_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{\xi,t,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}_{\xi,t,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot u_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down-shut-bigM` @@ -4864,7 +4980,7 @@ Generator_p_ramp_limit_down_shut_big_m: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_previous_p - Generator_p <= @@ -4873,7 +4989,7 @@ Generator_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{\xi,g} \cdot P_{g} + \mathrm{M}_{\xi,g} - \mathrm{M}_{\xi,g} \cdot \mathit{dn}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \wedge \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p_nom_modularity` @@ -5474,7 +5590,7 @@ Link_p_ramp_limit_up_run_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_p - Link_previous_p <= @@ -5483,7 +5599,7 @@ Link_p_ramp_limit_up_run_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{\xi,t,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f}_{\xi,t,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \overleftarrow{u}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_up-start-bigM` @@ -5500,7 +5616,7 @@ Link_p_ramp_limit_up_start_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_p - Link_previous_p <= @@ -5509,7 +5625,7 @@ Link_p_ramp_limit_up_start_big_m: ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \widetilde{\mathrm{ru}}^{f,\mathrm{up}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{up}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-run-bigM` @@ -5526,7 +5642,7 @@ Link_p_ramp_limit_down_run_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_previous_p - Link_p <= @@ -5535,7 +5651,7 @@ Link_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{\xi,t,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f}_{\xi,t,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot u^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down-shut-bigM` @@ -5552,7 +5668,7 @@ Link_p_ramp_limit_down_shut_big_m: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_previous_p - Link_p <= @@ -5561,7 +5677,7 @@ Link_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \widetilde{\mathrm{rd}}^{f,\mathrm{dn}}_{\xi,l} \cdot F_{l} + \mathrm{M}^{f}_{\xi,l} - \mathrm{M}^{f}_{\xi,l} \cdot \mathit{dn}^{f}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \wedge \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p_nom_modularity` @@ -6162,7 +6278,7 @@ Process_p_ramp_limit_up_run_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_p - Process_previous_p <= @@ -6171,7 +6287,7 @@ Process_p_ramp_limit_up_run_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{\xi,t,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z}_{\xi,t,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \overleftarrow{u}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_up-start-bigM` @@ -6188,7 +6304,7 @@ Process_p_ramp_limit_up_start_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_p - Process_previous_p <= @@ -6197,7 +6313,7 @@ Process_p_ramp_limit_up_start_big_m: ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \widetilde{\mathrm{ru}}^{z,\mathrm{up}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{up}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-run-bigM` @@ -6214,7 +6330,7 @@ Process_p_ramp_limit_down_run_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_previous_p - Process_p <= @@ -6223,7 +6339,7 @@ Process_p_ramp_limit_down_run_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{\xi,t,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z}_{\xi,t,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot u^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down-shut-bigM` @@ -6240,7 +6356,7 @@ Process_p_ramp_limit_down_shut_big_m: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_previous_p - Process_p <= @@ -6249,7 +6365,7 @@ Process_p_ramp_limit_down_shut_big_m: ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \widetilde{\mathrm{rd}}^{z,\mathrm{dn}}_{\xi,j} \cdot Z_{j} + \mathrm{M}^{z}_{\xi,j} - \mathrm{M}^{z}_{\xi,j} \cdot \mathit{dn}^{z}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \wedge \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p_nom_modularity` @@ -7272,19 +7388,20 @@ Generator_p_ramp_limit_up: `Generator-p-ramp_limit_up` — a generator raises output no faster than its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in, and no unit carries one at + the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance ``` ```math -p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +p_{\xi,t,g} - \overleftarrow{p}_{\xi,t,g} \le \Delta^{+}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \right) \wedge \mathrm{on}_{t,g} ``` ### `Generator-p-ramp_limit_down` @@ -7297,19 +7414,20 @@ Generator_p_ramp_limit_down: `Generator-p-ramp_limit_down` — a generator lowers output no faster than its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in, and no unit carries one at + the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance ``` ```math -\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}_{g} \wedge \mathrm{u}^{0}_{\xi,g} = 0 \right) \wedge \mathrm{on}_{t,g} +\overleftarrow{p}_{\xi,t,g} - p_{\xi,t,g} \le \Delta^{-}_{\xi,t,g} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{\xi,g} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}_{g} \wedge \mathrm{ext}_{g} \wedge \neg \left( \mathrm{p}^{\mathrm{mod}}_{g} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \right) \wedge \mathrm{on}_{t,g} ``` ### `Link-p-ramp_limit_up` @@ -7322,19 +7440,20 @@ Link_p_ramp_limit_up: `Link-p-ramp_limit_up` — a link raises flow no faster than its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A link that came into the - horizon running brought an unknown flow, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the flow it brought in, and no link carries one at + the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: Link_p - Link_previous_p <= Link_ramp_up_allowance ``` ```math -f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +f_{\xi,t,l} - \overleftarrow{f}_{\xi,t,l} \le \Delta^{f,+}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{ru}^{f,\mathrm{up}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Link-p-ramp_limit_down` @@ -7347,19 +7466,20 @@ Link_p_ramp_limit_down: `Link-p-ramp_limit_down` — a link lowers flow no faster than its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A link that came into the - horizon running brought an unknown flow, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the flow it brought in, and no link carries one at + the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: Link_previous_p - Link_p <= Link_ramp_down_allowance ``` ```math -\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{f}_{l} \wedge \mathrm{u}^{f,0}_{\xi,l} = 0 \right) \wedge \mathrm{on}^{f}_{t,l} +\overleftarrow{f}_{\xi,t,l} - f_{\xi,t,l} \le \Delta^{f,-}_{\xi,t,l} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \left( \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f,\mathrm{dn}}_{\xi,l} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{f}_{l} \wedge \mathrm{ext}^{f}_{l} \wedge \neg \left( \mathrm{f}^{\mathrm{mod}}_{l} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \right) \wedge \mathrm{on}^{f}_{t,l} ``` ### `Process-p-ramp_limit_up` @@ -7372,19 +7492,20 @@ Process_p_ramp_limit_up: `Process-p-ramp_limit_up` — a process raises internal power no faster than its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A process that came into the - horizon running brought an unknown internal power, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the internal power it brought in, and no process carries one at + the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: Process_p - Process_previous_p <= Process_ramp_up_allowance ``` ```math -z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +z_{\xi,t,j} - \overleftarrow{z}_{\xi,t,j} \le \Delta^{z,+}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{ru}^{z,\mathrm{up}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `Process-p-ramp_limit_down` @@ -7397,19 +7518,20 @@ Process_p_ramp_limit_down: `Process-p-ramp_limit_down` — a process lowers internal power no faster than its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A process that came into the - horizon running brought an unknown internal power, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the internal power it brought in, and no process carries one at + the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: Process_previous_p - Process_p <= Process_ramp_down_allowance ``` ```math -\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \mathrm{com}^{z}_{j} \wedge \mathrm{u}^{z,0}_{\xi,j} = 0 \right) \wedge \mathrm{on}^{z}_{t,j} +\overleftarrow{z}_{\xi,t,j} - z_{\xi,t,j} \le \Delta^{z,-}_{\xi,t,j} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \left( \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z,\mathrm{dn}}_{\xi,j} \text{ is defined} \right) \wedge \neg \left( \mathrm{com}^{z}_{j} \wedge \mathrm{ext}^{z}_{j} \wedge \neg \left( \mathrm{z}^{\mathrm{mod}}_{j} > 0 \right) \right) \wedge \left( \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \right) \wedge \mathrm{on}^{z}_{t,j} ``` ### `StorageUnit-ext-p_dispatch-lower` @@ -8214,17 +8336,17 @@ Generator_previous_status: ```yaml Generator_previous_p: description: >- - the output a generator carries into a snapshot — nothing at the start of - the horizon, which is why a unit that came in running carries no ramp row - there + the output a generator carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [scenario, snapshot, generator] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Generator_status_initial * Generator_p_init } otherwise: shift(Generator_p, along=snapshot, offset=1) ``` ```math -\overleftarrow{p}_{\xi,t,g} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ p_{\xi,t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} +\overleftarrow{p}_{\xi,t,g} = \begin{cases} \mathrm{u}^{0}_{\xi,g} \cdot \mathrm{p}^{0}_{\xi,g} & \text{if } \mathrm{pos}(t) = 0 \\ p_{\xi,t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_p_nom_effective` @@ -8410,17 +8532,17 @@ Link_previous_status: ```yaml Link_previous_p: description: >- - the flow a link carries into a snapshot — nothing at the start of - the horizon, which is why a link that came in running carries no ramp row - there + the flow a link carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [scenario, snapshot, link] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Link_status_initial * Link_p_init } otherwise: shift(Link_p, along=snapshot, offset=1) ``` ```math -\overleftarrow{f}_{\xi,t,l} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ f_{\xi,t - 1,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} +\overleftarrow{f}_{\xi,t,l} = \begin{cases} \mathrm{u}^{f,0}_{\xi,l} \cdot \mathrm{f}^{0}_{\xi,l} & \text{if } \mathrm{pos}(t) = 0 \\ f_{\xi,t - 1,l} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} ``` ### `Link_ramp_up_rate` @@ -8591,17 +8713,17 @@ Process_previous_status: ```yaml Process_previous_p: description: >- - the internal power a process carries into a snapshot — nothing at the start of - the horizon, which is why a process that came in running carries no ramp row - there + the internal power a process carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [scenario, snapshot, process] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Process_status_initial * Process_p_init } otherwise: shift(Process_p, along=snapshot, offset=1) ``` ```math -\overleftarrow{z}_{\xi,t,j} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ z_{\xi,t - 1,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} +\overleftarrow{z}_{\xi,t,j} = \begin{cases} \mathrm{u}^{z,0}_{\xi,j} \cdot \mathrm{z}^{0}_{\xi,j} & \text{if } \mathrm{pos}(t) = 0 \\ z_{\xi,t - 1,j} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} ``` ### `Process_ramp_up_rate` @@ -10025,6 +10147,66 @@ GlobalConstraint_tech_capacity_expansion_limit_without_scenarios: ```math \mathrm{type}_{i} \neq \text{'}\mathrm{tech\_capacity\_expansion\_limit}\text{'} \qquad \forall\, i \in \mathcal{I} \,:\, \lvert \{ \xi \in \Xi \,:\, \pi_{\xi} \text{ is defined} \} \rvert > 1 ``` + +### `Generator_came_in_running_unless_committable` + +```yaml +Generator_came_in_running_unless_committable: + holds: "Generator_status_initial == 1" + where: "NOT Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a unit that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the unit there at zero, or at its + start-up ramp where another unit of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable unit and does not check + it. The spec does not state that row, so it refuses the data +``` + +```math +\mathrm{u}^{0}_{\xi,g} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{com}_{g} \wedge \left( \mathrm{ru}_{\xi,t,g} \text{ is defined} \vee \mathrm{rd}_{\xi,t,g} \text{ is defined} \right) +``` + +### `Link_came_in_running_unless_committable` + +```yaml +Link_came_in_running_unless_committable: + holds: "Link_status_initial == 1" + where: "NOT Link_committable AND (Link_ramp_limit_up OR Link_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a link that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the link there at zero, or at its + start-up ramp where another link of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable link and does not check + it. The spec does not state that row, so it refuses the data +``` + +```math +\mathrm{u}^{f,0}_{\xi,l} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ l \in \mathcal{L} \,:\, \neg \mathrm{com}^{f}_{l} \wedge \left( \mathrm{ru}^{f}_{\xi,t,l} \text{ is defined} \vee \mathrm{rd}^{f}_{\xi,t,l} \text{ is defined} \right) +``` + +### `Process_came_in_running_unless_committable` + +```yaml +Process_came_in_running_unless_committable: + holds: "Process_status_initial == 1" + where: "NOT Process_committable AND (Process_ramp_limit_up OR Process_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a process that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the process there at zero, or at its + start-up ramp where another process of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable process and does not check + it. The spec does not state that row, so it refuses the data +``` + +```math +\mathrm{u}^{z,0}_{\xi,j} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ j \in \mathcal{J} \,:\, \neg \mathrm{com}^{z}_{j} \wedge \left( \mathrm{ru}^{z}_{\xi,t,j} \text{ is defined} \vee \mathrm{rd}^{z}_{\xi,t,j} \text{ is defined} \right) +``` Regenerate with `pixi run python -m tools.gallery`. diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index dc64f276..70cbeee9 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -129,6 +129,12 @@ parameters: description: one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep dims: [scenario, generator] dtype: int + Generator_p_init: + description: >- + the output a unit brought into the horizon — PyPSA's `p_init`, read + only where the unit came in running; no value means it is unknown, so + the unit carries no ramp row at the first snapshot + dims: [scenario, generator] Generator_must_stay_up: description: >- true while the up time a unit brought into the horizon still binds — @@ -270,6 +276,12 @@ parameters: description: one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep dims: [scenario, link] dtype: int + Link_p_init: + description: >- + the flow a link brought into the horizon — PyPSA's `p_init`, read + only where the link came in running; no value means it is unknown, so + the link carries no ramp row at the first snapshot + dims: [scenario, link] Link_must_stay_up: description: >- true while the up time a link brought into the horizon still binds — @@ -426,6 +438,12 @@ parameters: description: one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep dims: [scenario, process] dtype: int + Process_p_init: + description: >- + the internal power a process brought into the horizon — PyPSA's `p_init`, read + only where the process came in running; no value means it is unknown, so + the process carries no ramp row at the first snapshot + dims: [scenario, process] Process_must_stay_up: description: >- true while the up time a process brought into the horizon still binds — @@ -1597,12 +1615,12 @@ expressions: otherwise: shift(Generator_status, along=snapshot, offset=1) Generator_previous_p: description: >- - the output a generator carries into a snapshot — nothing at the start of - the horizon, which is why a unit that came in running carries no ramp row - there + the output a generator carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [scenario, snapshot, generator] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Generator_status_initial * Generator_p_init } otherwise: shift(Generator_p, along=snapshot, offset=1) Generator_p_nom_effective: description: the build a generator's limits are taken against — the chosen one where it is extendable, the given one otherwise @@ -1694,12 +1712,12 @@ expressions: otherwise: shift(Link_status, along=snapshot, offset=1) Link_previous_p: description: >- - the flow a link carries into a snapshot — nothing at the start of - the horizon, which is why a link that came in running carries no ramp row - there + the flow a link carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [scenario, snapshot, link] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Link_status_initial * Link_p_init } otherwise: shift(Link_p, along=snapshot, offset=1) Link_ramp_up_rate: description: >- @@ -1785,12 +1803,12 @@ expressions: otherwise: shift(Process_status, along=snapshot, offset=1) Process_previous_p: description: >- - the internal power a process carries into a snapshot — nothing at the start of - the horizon, which is why a process that came in running carries no ramp row - there + the internal power a process carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [scenario, snapshot, process] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Process_status_initial * Process_p_init } otherwise: shift(Process_p, along=snapshot, offset=1) Process_ramp_up_rate: description: >- @@ -2318,7 +2336,7 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_p - Generator_previous_p <= @@ -2333,7 +2351,7 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_p - Generator_previous_p <= @@ -2348,7 +2366,7 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_previous_p - Generator_p <= @@ -2363,7 +2381,7 @@ constraints: where: >- Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0) AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Generator_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: >- Generator_previous_p - Generator_p <= @@ -2588,7 +2606,7 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_p - Link_previous_p <= @@ -2603,7 +2621,7 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_up OR Link_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_p - Link_previous_p <= @@ -2618,7 +2636,7 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_previous_p - Link_p <= @@ -2633,7 +2651,7 @@ constraints: where: >- Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0) AND (Link_ramp_limit_down OR Link_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Link_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: >- Link_previous_p - Link_p <= @@ -2858,7 +2876,7 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_p - Process_previous_p <= @@ -2873,7 +2891,7 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_up OR Process_ramp_limit_start_up) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_p - Process_previous_p <= @@ -2888,7 +2906,7 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_previous_p - Process_p <= @@ -2903,7 +2921,7 @@ constraints: where: >- Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0) AND (Process_ramp_limit_down OR Process_ramp_limit_shut_down) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND Process_status_initial == 0)) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: >- Process_previous_p - Process_p <= @@ -3297,13 +3315,14 @@ constraints: `Generator-p-ramp_limit_up` — a generator raises output no faster than its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in, and no unit carries one at + the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance Generator_p_ramp_limit_down: @@ -3311,13 +3330,14 @@ constraints: `Generator-p-ramp_limit_down` — a generator lowers output no faster than its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any unit a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in, and no unit carries one at + the start of a later investment period — nor does any unit a big M releases instead dims: [scenario, snapshot, generator] where: >- (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) AND NOT (Generator_committable AND Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_committable AND Generator_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init))) AND Generator_active expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance Link_p_ramp_limit_up: @@ -3325,13 +3345,14 @@ constraints: `Link-p-ramp_limit_up` — a link raises flow no faster than its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A link that came into the - horizon running brought an unknown flow, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the flow it brought in, and no link carries one at + the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_up OR Link_ramp_limit_start_up) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: Link_p - Link_previous_p <= Link_ramp_up_allowance Link_p_ramp_limit_down: @@ -3339,13 +3360,14 @@ constraints: `Link-p-ramp_limit_down` — a link lowers flow no faster than its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A link that came into the - horizon running brought an unknown flow, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any link a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the flow it brought in, and no link carries one at + the start of a later investment period — nor does any link a big M releases instead dims: [scenario, snapshot, link] where: >- (Link_ramp_limit_down OR Link_ramp_limit_shut_down) AND NOT (Link_committable AND Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_committable AND Link_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init))) AND Link_active expression: Link_previous_p - Link_p <= Link_ramp_down_allowance Process_p_ramp_limit_up: @@ -3353,13 +3375,14 @@ constraints: `Process-p-ramp_limit_up` — a process raises internal power no faster than its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A process that came into the - horizon running brought an unknown internal power, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the internal power it brought in, and no process carries one at + the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_up OR Process_ramp_limit_start_up) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: Process_p - Process_previous_p <= Process_ramp_up_allowance Process_p_ramp_limit_down: @@ -3367,13 +3390,14 @@ constraints: `Process-p-ramp_limit_down` — a process lowers internal power no faster than its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A process that came into the - horizon running brought an unknown internal power, so it carries no row at the - first snapshot, nor at the start of a later investment period — nor does any process a big M releases instead + horizon running carries a row at the first snapshot only where its + `p_init` gives the internal power it brought in, and no process carries one at + the start of a later investment period — nor does any process a big M releases instead dims: [scenario, snapshot, process] where: >- (Process_ramp_limit_down OR Process_ramp_limit_shut_down) AND NOT (Process_committable AND Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_committable AND Process_status_initial == 0))) + AND (position(snapshot, by=snapshot_period, within=period) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init))) AND Process_active expression: Process_previous_p - Process_p <= Process_ramp_down_allowance StorageUnit_ext_p_dispatch_lower: @@ -3906,6 +3930,39 @@ assumptions: (`global_constraints.py:66-68`). The spec cannot tell a network with one scenario from one with none, so it refuses only where there is more than one scenario + Generator_came_in_running_unless_committable: + holds: "Generator_status_initial == 1" + where: "NOT Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a unit that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the unit there at zero, or at its + start-up ramp where another unit of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable unit and does not check + it. The spec does not state that row, so it refuses the data + Link_came_in_running_unless_committable: + holds: "Link_status_initial == 1" + where: "NOT Link_committable AND (Link_ramp_limit_up OR Link_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a link that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the link there at zero, or at its + start-up ramp where another link of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable link and does not check + it. The spec does not state that row, so it refuses the data + Process_came_in_running_unless_committable: + holds: "Process_status_initial == 1" + where: "NOT Process_committable AND (Process_ramp_limit_up OR Process_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a process that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the process there at zero, or at its + start-up ramp where another process of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable process and does not check + it. The spec does not state that row, so it refuses the data objective: sense: minimize diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index bd284b9d..a057da29 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1728,5 +1728,55 @@ "Process-p-ramp_limit_down": 2, "Process-p-ramp_limit_up": 3 } + }, + "rung_46_initial_output": { + "columns": { + "Generator-p": 28, + "Generator-p_nom": 2, + "Generator-shut_down": 8, + "Generator-start_up": 8, + "Generator-status": 8, + "Link-p": 8, + "Process-p": 4 + }, + "global_constraints": {}, + "objective": 101294.125, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-com-ext-p-lower": 4, + "Generator-com-ext-p-lower-nonneg": 4, + "Generator-com-ext-p-upper-bigM": 4, + "Generator-com-ext-p-upper-cap": 4, + "Generator-com-p-lower": 4, + "Generator-com-p-upper": 4, + "Generator-com-status-min_up_time_must_stay_up": 0, + "Generator-com-transition-shut-down": 8, + "Generator-com-transition-start-up": 8, + "Generator-ext-p-lower": 4, + "Generator-ext-p-upper": 4, + "Generator-ext-p_nom-lower": 2, + "Generator-ext-p_nom-upper": 2, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Generator-p-ramp_limit_down": 12, + "Generator-p-ramp_limit_down-run-bigM": 4, + "Generator-p-ramp_limit_down-shut-bigM": 4, + "Generator-p-ramp_limit_up": 12, + "Generator-p-ramp_limit_up-run-bigM": 4, + "Generator-p-ramp_limit_up-start-bigM": 4, + "Generator-shut_down-p-fixed-upper": 8, + "Generator-start_up-p-fixed-upper": 8, + "Generator-status-p-fixed-upper": 8, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8, + "Link-p-ramp_limit_down": 4, + "Link-p-ramp_limit_up": 4, + "Process-fix-p-lower": 4, + "Process-fix-p-upper": 4, + "Process-p-ramp_limit_down": 4, + "Process-p-ramp_limit_up": 4 + } } } diff --git a/examples/references/pypsa/rung_46_initial_output.py b/examples/references/pypsa/rung_46_initial_output.py new file mode 100644 index 00000000..737e2b04 --- /dev/null +++ b/examples/references/pypsa/rung_46_initial_output.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 46: the output brought in — units that came in running with a given `p_init` ramp from it into the first snapshot.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus a warm bus served by fixed, extendable and committable units that each carry a `p_init`, with a dear backup.""" + n = spine.build() + n.add('Bus', 'warm') + ramps = {'ramp_limit_up': 0.25, 'ramp_limit_down': 0.25} + n.add('Generator', 'warm_gen', bus='warm', p_nom=60, marginal_cost=3, p_init=10, **ramps) + n.add('Link', 'warm_link', bus0='north', bus1='warm', p_nom=40, marginal_cost=4, p_init=0, **ramps) + n.add( + 'Process', 'warm_proc', bus0='south', bus1='warm', rate0=-1.25, p_nom=40, marginal_cost=600, p_init=40, **ramps + ) + n.add( + 'Generator', + 'warm_ext', + bus='warm', + p_nom_extendable=True, + p_nom_max=40, + capital_cost=5, + marginal_cost=2, + p_init=5, + **ramps, + ) + n.add( + 'Generator', + 'warm_com', + bus='warm', + committable=True, + p_nom=50, + p_min_pu=0.2, + marginal_cost=2.5, + p_init=30, + ramp_limit_start_up=0.4, + ramp_limit_shut_down=0.4, + **ramps, + ) + n.add( + 'Generator', + 'warm_com_ext', + bus='warm', + committable=True, + p_nom_extendable=True, + p_nom_max=40, + capital_cost=5, + marginal_cost=2.2, + p_init=5, + **ramps, + ) + n.add('Generator', 'warm_backup', bus='warm', p_nom=300, marginal_cost=500) + n.add('Load', 'warm_load', bus='warm', p_set=[150, 150, 60, 60]) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 4ed2b4fc..621c9fd4 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -44,6 +44,7 @@ names: Generator_min_up_time: '\mathrm{UT}' Generator_min_down_time: '\mathrm{DT}' Generator_status_initial: '\mathrm{u}^{0}' + Generator_p_init: '\mathrm{p}^{0}' Generator_previous_status: '\overleftarrow{u}' Generator_previous_p: '\overleftarrow{p}' Generator_ramp_up_allowance: '\Delta^{+}' @@ -84,6 +85,7 @@ names: Link_min_up_time: '\mathrm{UT}^{f}' Link_min_down_time: '\mathrm{DT}^{f}' Link_status_initial: '\mathrm{u}^{f,0}' + Link_p_init: '\mathrm{f}^{0}' Link_previous_status: '\overleftarrow{u}^{f}' Link_previous_p: '\overleftarrow{f}' Link_ramp_up_allowance: '\Delta^{f,+}' @@ -153,6 +155,7 @@ names: Process_min_up_time: '\mathrm{UT}^{z}' Process_min_down_time: '\mathrm{DT}^{z}' Process_status_initial: '\mathrm{u}^{z,0}' + Process_p_init: '\mathrm{z}^{0}' Process_previous_status: '\overleftarrow{u}^{z}' Process_previous_p: '\overleftarrow{z}' Process_ramp_up_allowance: '\Delta^{z,+}' From 9998ede5ce0e5efe0d0cd7f252c091667c9dcb8e Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 08:28:26 +0200 Subject: [PATCH 36/46] 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/examples/pypsa.md | 2 +- docs/examples/pypsa_linearized_uc.md | 303 ++++++++++++++---- examples/pypsa_linearized_uc.yaml | 140 +++++--- examples/references/pypsa/references.json | 34 ++ .../pypsa/rung_47_linearized_ramps.py | 64 ++++ examples/symbols/pypsa_linearized_uc.yaml | 5 + 6 files changed, 445 insertions(+), 103 deletions(-) create mode 100644 examples/references/pypsa/rung_47_linearized_ramps.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 36e23bc8..02ee344e 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -608,7 +608,7 @@ def build(): | [`{c}-com-status-min_up_time_must_stay_up`](#generator-com-status-min_up_time_must_stay_up) | done | the window is a prep mask — `position()` takes a literal | | [`{c}-com-status-min_down_time_must_stay_up`](#generator-com-status-min_down_time_must_stay_up) | done | the same prep mask over the down time brought in, status zero; PyPSA's name says `_must_stay_up`; rung 24 records it | | [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | -| [`{c}-com-p-before/-current/-partly-*`](pypsa_linearized_uc.md) | done | rung 12, a file of its own | +| [`{c}-com-p-before/-current/-partly-*`](pypsa_linearized_uc.md) | done | rungs 12, 44 and 47, a file of its own: Generator commitment on fixed builds | > ✔ `pypsa 1.3.0` solves this rung's network at objective `7775.0`, 116 rows. diff --git a/docs/examples/pypsa_linearized_uc.md b/docs/examples/pypsa_linearized_uc.md index 634322e5..64e7da22 100644 --- a/docs/examples/pypsa_linearized_uc.md +++ b/docs/examples/pypsa_linearized_uc.md @@ -5,9 +5,15 @@ SPDX-License-Identifier: CC-BY-4.0 # PyPSA, the relaxed commitment -Rungs 12 and 44 of [PyPSA in one file](pypsa.md): `n.optimize(linearized_unit_commitment=True)`, stated on rungs 1 and 7 +Rungs 12, 44 and 47 of [PyPSA in one file](pypsa.md): `n.optimize(linearized_unit_commitment=True)`, stated on rungs 1 and 7 in a file of its own. Each network is the spine plus the script's own additions. +The file covers generators, links and loads with a fixed build, in one +scenario, with every asset active in every snapshot. Only a generator is +committable. A committable link or process, an extendable build, scenarios and +`active` are out of this file's scope. [PyPSA in one file](pypsa.md) states +them for the integer run, and the keyword relaxes them in the same way. + ## Rung 12 — linearized unit commitment | PyPSA | status | note | @@ -122,8 +128,6 @@ maintainable, to `7310.0`; with the fixed unit not maintainable, to `7100.0`. | [`Generator-p-ramp_limit_up`](#generator-p-ramp_limit_up), [`-down`](#generator-p-ramp_limit_down) for a start-up or shut-down ramp alone, and a missing limit | done | `Generator_ramp_up_rate` and the other three rates read a missing limit as `1` | | [`Generator-maint-*`](#generator-maint-event-count), [`Generator-maint-status-*`](#generator-maint-status-le-status) | done | fixed units only, as the file states no extendable build | | [`Generator-com-p-lower`](#generator-com-p-lower), [`-upper`](#generator-com-p-upper), [`Generator-fix-p-lower`](#generator-fix-p-lower), [`-upper`](#generator-fix-p-upper) in maintenance | done | | -| [`Generator-com-p-before`](#generator-com-p-before), [`-current`](#generator-com-p-current), [`-partly-start-up`](#generator-com-partly-start-up), [`-partly-shut-down`](#generator-com-partly-shut-down) with a missing ramp limit | out | PyPSA reads a missing limit as the full build (`constraints.py:311-316`); the file reads it as `0` | -| a ramp row for a unit that is not committable | out | PyPSA builds it with the status fixed at `1` (`constraints.py:1073-1079`); the file states ramp rows for committable units only | > ✔ `pypsa 1.3.0` solves this rung's network at objective `7400.0`, 191 rows. @@ -194,10 +198,129 @@ def build(): +## Rung 47 — ramps and signs, as the integer file states them + +This rung brings five rows of [PyPSA in one file](pypsa.md) into this file, +each on the fixed builds of this file's surface: + +- The four tightening rows read each ramp limit filled to the full build where + it is missing (`constraints.py:313-318`). They read `Generator_ramp_up_rate` + and the other three rates. The earlier file read a missing limit as `0`. +- A ramp limit is given per snapshot and read at the later of the two + snapshots (`constraints.py:1040-1041`). A row is absent at a snapshot where + neither the limit nor the start-up or shut-down ramp has a value + (`constraints.py:1046-1047`, `1136`, `1154`). +- A unit that came in running with a `p_init` ramps from it into the first + snapshot (`constraints.py:1091-1094`, `1102-1104`). Where `p_init` has no + value, the unit carries no ramp row at the first snapshot. +- A unit that is not committable carries ramp rows with its status fixed at + `1` (`constraints.py:1073-1079`). The assumption + `Generator_came_in_running_unless_committable` refuses such a unit that came + in off, as the integer file does. +- Each generator and load term enters the bus balance with its component's + `sign` (`constraints.py:1428-1429`, `:1538`). + +The rung adds a relax bus with a tightened unit that has only start-up and +shut-down ramps, a committable unit whose ramp limit lifts at the third +snapshot, a dear committable unit that came in running at `p_init=40`, a fixed +unit with ramp limits, a unit of sign `-1`, a load of sign `1` and a dear +backup. Each binds: PyPSA solves to `12862.5`. With the missing limits read as +`0` in the two partly rows, it solves to `14085.64`. With the ramp limit at +`0.25` in every snapshot, it solves to `14554.75`. Without the `p_init`, it +solves to `8437.5`. Without the fixed unit's ramp limits, it solves to +`12800.0`. With the unit's sign at `1`, it solves to `12592.5`. With the load's +sign at `-1`, it solves to `13887.5`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Generator-com-p-before`](#generator-com-p-before), [`-current`](#generator-com-p-current), [`-partly-start-up`](#generator-com-partly-start-up), [`-partly-shut-down`](#generator-com-partly-shut-down) with a missing ramp limit | done | the four rates read a missing limit as `1` | +| [`Generator-p-ramp_limit_up`](#generator-p-ramp_limit_up), [`-down`](#generator-p-ramp_limit_down) per snapshot | done | `Generator_ramp_limit_up` and `-down` over `[snapshot, generator]` | +| [`Generator-p-ramp_limit_up`](#generator-p-ramp_limit_up), [`-down`](#generator-p-ramp_limit_down) at the first snapshot from `p_init` | done | `Generator_previous_p` opens at `Generator_status_initial * Generator_p_init` | +| [`Generator-p-ramp_limit_up`](#generator-p-ramp_limit_up), [`-down`](#generator-p-ramp_limit_down) for a unit that is not committable | done | `Generator_ramp_up_allowance` and `-down` read the status as `1` | +| [`Bus-nodal_balance`](#bus-nodal_balance) with `sign` | done | `Generator_sign` and `Load_sign` | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `12862.5`, 179 rows. + +
+The network, as PyPSA code + +`rung_47_linearized_ramps.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 47: the relaxed file's ramps and signs — tightening at the full build, a ramp limit per snapshot, a `p_init`, a unit that is not committable, and a sign.""" + +from __future__ import annotations + +import math + +import spine + +MODEL = 'pypsa_linearized_uc.yaml' +OPTIMIZE = {'linearized_unit_commitment': True} + + +def build(): + """The spine plus a relax bus: five units that each carry one of the rows under review, a feeding load and a dear backup.""" + n = spine.build() + n.add('Bus', 'relax') + n.add( + 'Generator', + 'tight47', + bus='relax', + committable=True, + p_nom=50, + p_min_pu=0.2, + marginal_cost=2, + up_time_before=0, + ramp_limit_start_up=0.5, + ramp_limit_shut_down=0.5, + start_up_cost=10, + shut_down_cost=10, + ) + n.add( + 'Generator', + 'steep47', + bus='relax', + committable=True, + p_nom=40, + marginal_cost=3, + start_up_cost=1, + ramp_limit_up=[0.25, 0.25, math.nan, 0.25], + ramp_limit_down=[0.25, 0.25, 0.25, math.nan], + ramp_limit_start_up=0.25, + ) + n.add( + 'Generator', + 'warm47', + bus='relax', + committable=True, + p_nom=40, + marginal_cost=50, + start_up_cost=1, + p_init=40, + ramp_limit_down=0.25, + ramp_limit_shut_down=0.5, + ) + n.add('Generator', 'fixed47', bus='relax', p_nom=60, marginal_cost=1, ramp_limit_up=0.25, ramp_limit_down=0.25) + n.add('Generator', 'sink47', bus='relax', p_nom=20, p_min_pu=0.5, sign=-1) + n.add('Generator', 'backup47', bus='relax', p_nom=300, marginal_cost=500) + n.add('Load', 'feed47', bus='relax', p_set=10, sign=1) + n.add('Load', 'swing47', bus='relax', p_set=[60, 60, 140, 40]) + return n +``` + +
+ + ## The file -The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, stated on rung 1's transport surface in a file of its own. The status, its starts and its stops are shares in \[0, 1\] rather than binaries — a domain is the model's, not the data's — and four rows PyPSA adds only under the keyword tighten the relaxation where a unit's start and stop cost the same. `examples/pypsa.yaml` stays the integer one. +The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, stated on rung 1's transport surface in a file of its own. The status, its starts and its stops are shares in \[0, 1\] rather than binaries — a domain is the model's, not the data's — and four rows PyPSA adds only under the keyword tighten the relaxation where a unit's start and stop cost the same. The surface is generators, links and loads with a fixed build, in one scenario, every asset active in every snapshot, and only a generator committable. `examples/pypsa.yaml` stays the integer one, and states the rest: a committable link or process, an extendable build, scenarios and `active`. #### Sets @@ -220,20 +343,23 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state | $`\underline{\mathrm{p}}`$ | `Generator_p_min_pu` over $`\mathcal{T} \times \mathcal{G}`$ — least output, per unit of nominal power | | $`\overline{\mathrm{p}}`$ | `Generator_p_max_pu` over $`\mathcal{T} \times \mathcal{G}`$ — most output, per unit of nominal power — an availability profile | | $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | +| $`\mathrm{sgn}`$ | `Generator_sign` over $`\mathcal{G}`$ — the sign output enters its bus's balance with — PyPSA's `sign`, `1` unless given, `-1` for a unit that draws power | | $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\mathcal{L}`$ — nominal power | | $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | | $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | | $`\eta`$ | `Link_efficiency` over $`\mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | | $`\mathrm{load}`$ | `Load_p_set` over $`\mathcal{T} \times \mathcal{D}`$ — demand | +| $`\mathrm{sgn}^{\mathrm{load}}`$ | `Load_sign` over $`\mathcal{D}`$ — the sign a load's demand enters its bus's balance with — PyPSA's `sign`, `-1` unless given, `1` for a load that feeds its bus | | $`\mathrm{com}`$ | `Generator_committable` over $`\mathcal{G}`$ — whether output is gated by an on/off status decision | -| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit | -| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit | +| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\mathcal{T} \times \mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\mathcal{T} \times \mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | | $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\mathcal{G}`$ — most output in the snapshot a unit starts, per unit of nominal power | | $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\mathcal{G}`$ — most output in the snapshot before a unit stops, per unit of nominal power | | $`\mathrm{UT}`$ | `Generator_min_up_time` over $`\mathcal{G}`$ — least snapshots a unit stays on once started | | $`\mathrm{DT}`$ | `Generator_min_down_time` over $`\mathcal{G}`$ — least snapshots a unit stays off once stopped | | $`\mathrm{u}^{0}`$ | `Generator_status_initial` over $`\mathcal{G}`$ — one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | +| $`\mathrm{p}^{0}`$ | `Generator_p_init` over $`\mathcal{G}`$ — the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot | | $`\mathrm{hold}`$ | `Generator_must_stay_up` over $`\mathcal{T} \times \mathcal{G}`$ — true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | | $`\mathrm{rest}`$ | `Generator_must_stay_down` over $`\mathcal{T} \times \mathcal{G}`$ — true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\mathcal{G}`$ — cost of one start | @@ -264,11 +390,13 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state | Symbol | Meaning | |---|---| | $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | -| $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | -| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | -| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | +| $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — at the first, the `p_init` it brought in where it came in running and nothing where it came in off; the previous snapshot's after that | +| $`\widetilde{\mathrm{ru}}`$ | `Generator_ramp_up_rate` over $`\mathcal{T} \times \mathcal{G}`$ — the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row | +| $`\widetilde{\mathrm{rd}}`$ | `Generator_ramp_down_rate` over $`\mathcal{T} \times \mathcal{G}`$ — the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row | | $`\widetilde{\mathrm{ru}}^{\mathrm{up}}`$ | `Generator_start_up_rate` over $`\mathcal{G}`$ — the start-up ramp a unit's up row reads — PyPSA's `ramp_limit_start_up`, or the full build where it has none | | $`\widetilde{\mathrm{rd}}^{\mathrm{dn}}`$ | `Generator_shut_down_rate` over $`\mathcal{G}`$ — the shut-down ramp a unit's down row reads — PyPSA's `ramp_limit_shut_down`, or the full build where it has none | +| $`\Delta^{+}`$ | `Generator_ramp_up_allowance` over $`\mathcal{T} \times \mathcal{G}`$ — how far a generator may raise output between two snapshots — its ramp limit of the build while it stays on, plus its start-up ramp in the snapshot it turns on | +| $`\Delta^{-}`$ | `Generator_ramp_down_allowance` over $`\mathcal{T} \times \mathcal{G}`$ — how far a generator may lower output between two snapshots — its ramp limit of the build while it stays on, plus its shut-down ramp in the snapshot it turns off | $`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, not the order labels sort in — counted from $`0`$. The index itself stays the coordinate, so $`t`$ compares against labels and $`\mathrm{pos}(t)`$ against positions. @@ -361,17 +489,18 @@ Bus_nodal_balance: description: >- `Bus-nodal_balance` — what is generated at a bus, less what the links take away, plus what arrives over them after losses, meets the load - there + there. Each generator and load term enters with its component's `sign` + (`constraints.py:1428-1429`, `:1538`) dims: [snapshot, bus] expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) + sum(Generator_sign * Generator_p, by=Generator_bus, over=generator, into=bus) - sum(Link_p, by=Link_bus0, over=link, into=bus) + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) + == -sum(Load_sign * Load_p_set, by=Load_bus, over=load, into=bus) ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} p_{t,g} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} f_{t,\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{t,g} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} f_{t,\mathrm{Link\_output\_link}(o)} \cdot \eta_{o} = -\left( \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{sgn}^{\mathrm{load}}_{d} \cdot \mathrm{load}_{t,d} \right) \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} ``` ### `Generator-com-p-lower` @@ -517,24 +646,20 @@ u_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm ```yaml Generator_p_ramp_limit_up: description: >- - `Generator-p-ramp_limit_up` — a committed unit raises output no faster - than its limit while it was already on, and no further than its + `Generator-p-ramp_limit_up` — a generator raises output no faster than + its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in dims: [snapshot, generator] where: >- - Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) - expression: >- - Generator_p - Generator_previous_p <= - Generator_ramp_up_rate * Generator_p_nom * Generator_previous_status - + Generator_start_up_rate * Generator_p_nom - * (Generator_status - Generator_previous_status) + (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) + AND (position(snapshot) > 0 OR Generator_status_initial == 0 OR Generator_p_init) + expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance ``` ```math -p_{t,g} - \overleftarrow{p}_{t,g} \le \widetilde{\mathrm{ru}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{t,g} - \overleftarrow{u}_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \left( \mathrm{ru}_{g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +p_{t,g} - \overleftarrow{p}_{t,g} \le \Delta^{+}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{ru}_{t,g} \text{ is defined} \vee \mathrm{ru}^{\mathrm{up}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \vee \mathrm{p}^{0}_{g} \text{ is defined} \right) ``` ### `Generator-p-ramp_limit_down` @@ -544,24 +669,20 @@ p_{t,g} - \overleftarrow{p}_{t,g} \le \widetilde{\mathrm{ru}}_{g} \cdot \mathrm{ ```yaml Generator_p_ramp_limit_down: description: >- - `Generator-p-ramp_limit_down` — a committed unit lowers output no - faster than its limit while it stays on, and no further than its + `Generator-p-ramp_limit_down` — a generator lowers output no faster than + its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in dims: [snapshot, generator] where: >- - Generator_committable AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) - expression: >- - Generator_previous_p - Generator_p <= - Generator_ramp_down_rate * Generator_p_nom * Generator_status - + Generator_shut_down_rate * Generator_p_nom - * (Generator_previous_status - Generator_status) + (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) + AND (position(snapshot) > 0 OR Generator_status_initial == 0 OR Generator_p_init) + expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance ``` ```math -\overleftarrow{p}_{t,g} - p_{t,g} \le \widetilde{\mathrm{rd}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{t,g} - u_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \left( \mathrm{rd}_{g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \right) +\overleftarrow{p}_{t,g} - p_{t,g} \le \Delta^{-}_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{rd}_{t,g} \text{ is defined} \vee \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \right) \wedge \left( \mathrm{pos}(t) > 0 \vee \mathrm{u}^{0}_{g} = 0 \vee \mathrm{p}^{0}_{g} \text{ is defined} \right) ``` ### `Generator-status-p-fixed-upper` @@ -726,13 +847,13 @@ Generator_com_p_before: where: Generator_committable AND Generator_partly_tightened expression: >- shift(Generator_p, along=snapshot, offset=1) - - Generator_ramp_limit_shut_down * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) - - (Generator_p_max_pu * Generator_p_nom - Generator_ramp_limit_shut_down * Generator_p_nom) + - Generator_shut_down_rate * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) + - (Generator_p_max_pu * Generator_p_nom - Generator_shut_down_rate * Generator_p_nom) * (Generator_status - Generator_start_up) <= 0 ``` ```math -p_{t - 1,g} - \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t - 1,g} - \left( \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \left( u_{t,g} - \mathit{up}_{t,g} \right) \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} +p_{t - 1,g} - \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t - 1,g} - \left( \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \left( u_{t,g} - \mathit{up}_{t,g} \right) \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} ``` ### `Generator-com-p-current` @@ -746,11 +867,11 @@ Generator_com_p_current: where: Generator_committable AND Generator_partly_tightened AND position(snapshot) > 0 expression: >- Generator_p - Generator_p_max_pu * Generator_p_nom * Generator_status - + (Generator_p_max_pu * Generator_p_nom - Generator_ramp_limit_start_up * Generator_p_nom) * Generator_start_up <= 0 + + (Generator_p_max_pu * Generator_p_nom - Generator_start_up_rate * Generator_p_nom) * Generator_start_up <= 0 ``` ```math -p_{t,g} - \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} + \left( \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \mathrm{ru}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \mathit{up}_{t,g} \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} \wedge \mathrm{pos}(t) > 0 +p_{t,g} - \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} + \left( \overline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \mathit{up}_{t,g} \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} \wedge \mathrm{pos}(t) > 0 ``` ### `Generator-com-partly-start-up` @@ -764,14 +885,14 @@ Generator_com_partly_start_up: where: Generator_committable AND Generator_partly_tightened expression: >- Generator_p - shift(Generator_p, along=snapshot, offset=1) - - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_limit_up * Generator_p_nom) * Generator_status + - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom) * Generator_status + Generator_p_min_pu * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) - + (Generator_p_min_pu * Generator_p_nom + Generator_ramp_limit_up * Generator_p_nom - Generator_ramp_limit_start_up * Generator_p_nom) + + (Generator_p_min_pu * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom - Generator_start_up_rate * Generator_p_nom) * Generator_start_up <= 0 ``` ```math -p_{t,g} - p_{t - 1,g} - \left( \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} + \mathrm{ru}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot u_{t,g} + \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t - 1,g} + \left( \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} + \mathrm{ru}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \mathrm{ru}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \mathit{up}_{t,g} \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} +p_{t,g} - p_{t - 1,g} - \left( \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} + \widetilde{\mathrm{ru}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot u_{t,g} + \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t - 1,g} + \left( \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} + \widetilde{\mathrm{ru}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \mathit{up}_{t,g} \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} ``` ### `Generator-com-partly-shut-down` @@ -785,14 +906,14 @@ Generator_com_partly_shut_down: where: Generator_committable AND Generator_partly_tightened expression: >- shift(Generator_p, along=snapshot, offset=1) - Generator_p - - Generator_ramp_limit_shut_down * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) - + (Generator_ramp_limit_shut_down * Generator_p_nom - Generator_ramp_limit_down * Generator_p_nom) * Generator_status - - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_limit_down * Generator_p_nom - Generator_ramp_limit_shut_down * Generator_p_nom) + - Generator_shut_down_rate * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) + + (Generator_shut_down_rate * Generator_p_nom - Generator_ramp_down_rate * Generator_p_nom) * Generator_status + - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_down_rate * Generator_p_nom - Generator_shut_down_rate * Generator_p_nom) * Generator_start_up <= 0 ``` ```math -p_{t - 1,g} - p_{t,g} - \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t - 1,g} + \left( \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \mathrm{rd}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot u_{t,g} - \left( \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} + \mathrm{rd}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \mathrm{rd}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \mathit{up}_{t,g} \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} +p_{t - 1,g} - p_{t,g} - \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t - 1,g} + \left( \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \widetilde{\mathrm{rd}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot u_{t,g} - \left( \underline{\mathrm{p}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} + \widetilde{\mathrm{rd}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} - \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \right) \cdot \mathit{up}_{t,g} \le 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{com}_{g} \wedge \mathrm{tight}_{g} ``` ### `Generator_previous_status` @@ -817,17 +938,17 @@ Generator_previous_status: ```yaml Generator_previous_p: description: >- - the output a generator carries into a snapshot — nothing at the start of - the horizon, which is why a unit that came in running carries no ramp row - there + the output a generator carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [snapshot, generator] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Generator_status_initial * Generator_p_init } otherwise: shift(Generator_p, along=snapshot, offset=1) ``` ```math -\overleftarrow{p}_{t,g} = \begin{cases} 0 & \text{if } \mathrm{pos}(t) = 0 \\ p_{t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\overleftarrow{p}_{t,g} = \begin{cases} \mathrm{u}^{0}_{g} \cdot \mathrm{p}^{0}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ p_{t - 1,g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_ramp_up_rate` @@ -837,14 +958,14 @@ Generator_ramp_up_rate: description: >- the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [generator] + dims: [snapshot, generator] cases: given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } otherwise: 1 ``` ```math -\widetilde{\mathrm{ru}}_{g} = \begin{cases} \mathrm{ru}_{g} & \text{if } \mathrm{ru}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widetilde{\mathrm{ru}}_{t,g} = \begin{cases} \mathrm{ru}_{t,g} & \text{if } \mathrm{ru}_{t,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_ramp_down_rate` @@ -854,14 +975,14 @@ Generator_ramp_down_rate: description: >- the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [generator] + dims: [snapshot, generator] cases: given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } otherwise: 1 ``` ```math -\widetilde{\mathrm{rd}}_{g} = \begin{cases} \mathrm{rd}_{g} & \text{if } \mathrm{rd}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} +\widetilde{\mathrm{rd}}_{t,g} = \begin{cases} \mathrm{rd}_{t,g} & \text{if } \mathrm{rd}_{t,g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` ### `Generator_start_up_rate` @@ -898,6 +1019,52 @@ Generator_shut_down_rate: \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} = \begin{cases} \mathrm{rd}^{\mathrm{dn}}_{g} & \text{if } \mathrm{rd}^{\mathrm{dn}}_{g} \text{ is defined} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G} ``` +### `Generator_ramp_up_allowance` + +```yaml +Generator_ramp_up_allowance: + description: >- + how far a generator may raise output between two snapshots — its ramp + limit of the build while it stays on, plus its start-up ramp in the + snapshot it turns on + dims: [snapshot, generator] + cases: + committed: + when: Generator_committable + expression: >- + Generator_ramp_up_rate * Generator_p_nom * Generator_previous_status + + Generator_start_up_rate * Generator_p_nom + * (Generator_status - Generator_previous_status) + otherwise: Generator_ramp_up_rate * Generator_p_nom +``` + +```math +\Delta^{+}_{t,g} = \begin{cases} \widetilde{\mathrm{ru}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \overleftarrow{u}_{t,g} + \widetilde{\mathrm{ru}}^{\mathrm{up}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( u_{t,g} - \overleftarrow{u}_{t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{ru}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +``` + +### `Generator_ramp_down_allowance` + +```yaml +Generator_ramp_down_allowance: + description: >- + how far a generator may lower output between two snapshots — its ramp + limit of the build while it stays on, plus its shut-down ramp in the + snapshot it turns off + dims: [snapshot, generator] + cases: + committed: + when: Generator_committable + expression: >- + Generator_ramp_down_rate * Generator_p_nom * Generator_status + + Generator_shut_down_rate * Generator_p_nom + * (Generator_previous_status - Generator_status) + otherwise: Generator_ramp_down_rate * Generator_p_nom +``` + +```math +\Delta^{-}_{t,g} = \begin{cases} \widetilde{\mathrm{rd}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} + \widetilde{\mathrm{rd}}^{\mathrm{dn}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot \left( \overleftarrow{u}_{t,g} - u_{t,g} \right) & \text{if } \mathrm{com}_{g} \\ \widetilde{\mathrm{rd}}_{t,g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +``` + #### Variable domains **`Generator_p`** @@ -948,6 +1115,26 @@ u_{t,g} \ge 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \math \mu^{u}_{t,g} \ge 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{mnt}_{g} \wedge \mathrm{com}_{g} ``` +### `Generator_came_in_running_unless_committable` + +```yaml +Generator_came_in_running_unless_committable: + holds: "Generator_status_initial == 1" + where: "NOT Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a unit that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the unit there at zero, or at its + start-up ramp where another unit of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable unit and does not check + it. The spec does not state that row, so it refuses the data +``` + +```math +\mathrm{u}^{0}_{g} = 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \neg \mathrm{com}_{g} \wedge \left( \mathrm{ru}_{t,g} \text{ is defined} \vee \mathrm{rd}_{t,g} \text{ is defined} \right) +``` + ### `Generator_maintenance_events_positive` ```yaml diff --git a/examples/pypsa_linearized_uc.yaml b/examples/pypsa_linearized_uc.yaml index 6c11d5c4..2606c5b4 100644 --- a/examples/pypsa_linearized_uc.yaml +++ b/examples/pypsa_linearized_uc.yaml @@ -8,7 +8,11 @@ description: >- starts and its stops are shares in [0, 1] rather than binaries — a domain is the model's, not the data's — and four rows PyPSA adds only under the keyword tighten the relaxation where a unit's start and stop cost the same. - `examples/pypsa.yaml` stays the integer one. + The surface is generators, links and loads with a fixed build, in one + scenario, every asset active in every snapshot, and only a generator + committable. `examples/pypsa.yaml` stays the integer one, and states the + rest: a committable link or process, an extendable build, scenarios and + `active`. dimensions: snapshot: @@ -47,6 +51,11 @@ parameters: Generator_marginal_cost: description: cost of one unit of output dims: [snapshot, generator] + Generator_sign: + description: >- + the sign output enters its bus's balance with — PyPSA's `sign`, `1` + unless given, `-1` for a unit that draws power + dims: [generator] Link_p_nom: description: nominal power dims: [link] @@ -68,16 +77,21 @@ parameters: Load_p_set: description: demand dims: [snapshot, load] + Load_sign: + description: >- + the sign a load's demand enters its bus's balance with — PyPSA's + `sign`, `-1` unless given, `1` for a load that feeds its bus + dims: [load] Generator_committable: description: whether output is gated by an on/off status decision dims: [generator] dtype: bool Generator_ramp_limit_up: - description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit - dims: [generator] + description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [snapshot, generator] Generator_ramp_limit_down: - description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit - dims: [generator] + description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time + dims: [snapshot, generator] Generator_ramp_limit_start_up: description: most output in the snapshot a unit starts, per unit of nominal power dims: [generator] @@ -96,6 +110,12 @@ parameters: description: one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep dims: [generator] dtype: int + Generator_p_init: + description: >- + the output a unit brought into the horizon — PyPSA's `p_init`, read + only where the unit came in running; no value means it is unknown, so + the unit carries no ramp row at the first snapshot + dims: [generator] Generator_must_stay_up: description: >- true while the up time a unit brought into the horizon still binds — @@ -257,18 +277,18 @@ expressions: otherwise: shift(Generator_status, along=snapshot, offset=1) Generator_previous_p: description: >- - the output a generator carries into a snapshot — nothing at the start of - the horizon, which is why a unit that came in running carries no ramp row - there + the output a generator carries into a snapshot — at the first, the + `p_init` it brought in where it came in running and nothing where it + came in off; the previous snapshot's after that dims: [snapshot, generator] cases: - opening: { when: "position(snapshot) == 0", expression: 0 } + opening: { when: "position(snapshot) == 0", expression: Generator_status_initial * Generator_p_init } otherwise: shift(Generator_p, along=snapshot, offset=1) Generator_ramp_up_rate: description: >- the ramp limit a unit's up row reads — PyPSA's `ramp_limit_up`, or the full build where it has none, since a start-up ramp alone builds the row - dims: [generator] + dims: [snapshot, generator] cases: given: { when: Generator_ramp_limit_up, expression: Generator_ramp_limit_up } otherwise: 1 @@ -276,7 +296,7 @@ expressions: description: >- the ramp limit a unit's down row reads — PyPSA's `ramp_limit_down`, or the full build where it has none, since a shut-down ramp alone builds the row - dims: [generator] + dims: [snapshot, generator] cases: given: { when: Generator_ramp_limit_down, expression: Generator_ramp_limit_down } otherwise: 1 @@ -296,6 +316,34 @@ expressions: cases: given: { when: Generator_ramp_limit_shut_down, expression: Generator_ramp_limit_shut_down } otherwise: 1 + Generator_ramp_up_allowance: + description: >- + how far a generator may raise output between two snapshots — its ramp + limit of the build while it stays on, plus its start-up ramp in the + snapshot it turns on + dims: [snapshot, generator] + cases: + committed: + when: Generator_committable + expression: >- + Generator_ramp_up_rate * Generator_p_nom * Generator_previous_status + + Generator_start_up_rate * Generator_p_nom + * (Generator_status - Generator_previous_status) + otherwise: Generator_ramp_up_rate * Generator_p_nom + Generator_ramp_down_allowance: + description: >- + how far a generator may lower output between two snapshots — its ramp + limit of the build while it stays on, plus its shut-down ramp in the + snapshot it turns off + dims: [snapshot, generator] + cases: + committed: + when: Generator_committable + expression: >- + Generator_ramp_down_rate * Generator_p_nom * Generator_status + + Generator_shut_down_rate * Generator_p_nom + * (Generator_previous_status - Generator_status) + otherwise: Generator_ramp_down_rate * Generator_p_nom constraints: Generator_fix_p_lower: @@ -320,13 +368,14 @@ constraints: description: >- `Bus-nodal_balance` — what is generated at a bus, less what the links take away, plus what arrives over them after losses, meets the load - there + there. Each generator and load term enters with its component's `sign` + (`constraints.py:1428-1429`, `:1538`) dims: [snapshot, bus] expression: >- - sum(Generator_p, by=Generator_bus, over=generator, into=bus) + sum(Generator_sign * Generator_p, by=Generator_bus, over=generator, into=bus) - sum(Link_p, by=Link_bus0, over=link, into=bus) + sum(at(Link_p, by=Link_output_link, over=link, into=link_output) * Link_efficiency, by=Link_output_bus, over=link_output, into=bus) - == sum(Load_p_set, by=Load_bus, over=load, into=bus) + == -sum(Load_sign * Load_p_set, by=Load_bus, over=load, into=bus) Generator_com_p_lower: description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing" dims: [snapshot, generator] @@ -377,36 +426,28 @@ constraints: expression: Generator_status == 0 Generator_p_ramp_limit_up: description: >- - `Generator-p-ramp_limit_up` — a committed unit raises output no faster - than its limit while it was already on, and no further than its + `Generator-p-ramp_limit_up` — a generator raises output no faster than + its ramp limit of the build, and a committed one no further than its start-up ramp in the snapshot it turns on. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in dims: [snapshot, generator] where: >- - Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) - expression: >- - Generator_p - Generator_previous_p <= - Generator_ramp_up_rate * Generator_p_nom * Generator_previous_status - + Generator_start_up_rate * Generator_p_nom - * (Generator_status - Generator_previous_status) + (Generator_ramp_limit_up OR Generator_ramp_limit_start_up) + AND (position(snapshot) > 0 OR Generator_status_initial == 0 OR Generator_p_init) + expression: Generator_p - Generator_previous_p <= Generator_ramp_up_allowance Generator_p_ramp_limit_down: description: >- - `Generator-p-ramp_limit_down` — a committed unit lowers output no - faster than its limit while it stays on, and no further than its + `Generator-p-ramp_limit_down` — a generator lowers output no faster than + its ramp limit of the build, and a committed one no further than its shut-down ramp in the snapshot it turns off. A unit that came into the - horizon running brought an unknown output, so it carries no row at the - first snapshot + horizon running carries a row at the first snapshot only where its + `p_init` gives the output it brought in dims: [snapshot, generator] where: >- - Generator_committable AND (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) - AND (position(snapshot) > 0 OR Generator_status_initial == 0) - expression: >- - Generator_previous_p - Generator_p <= - Generator_ramp_down_rate * Generator_p_nom * Generator_status - + Generator_shut_down_rate * Generator_p_nom - * (Generator_previous_status - Generator_status) + (Generator_ramp_limit_down OR Generator_ramp_limit_shut_down) + AND (position(snapshot) > 0 OR Generator_status_initial == 0 OR Generator_p_init) + expression: Generator_previous_p - Generator_p <= Generator_ramp_down_allowance Generator_status_p_fixed_upper: description: "`Generator-status-p-fixed-upper` — a status is at most one, an explicit row as PyPSA writes it" dims: [snapshot, generator] @@ -465,8 +506,8 @@ constraints: where: Generator_committable AND Generator_partly_tightened expression: >- shift(Generator_p, along=snapshot, offset=1) - - Generator_ramp_limit_shut_down * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) - - (Generator_p_max_pu * Generator_p_nom - Generator_ramp_limit_shut_down * Generator_p_nom) + - Generator_shut_down_rate * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) + - (Generator_p_max_pu * Generator_p_nom - Generator_shut_down_rate * Generator_p_nom) * (Generator_status - Generator_start_up) <= 0 Generator_com_p_current: description: "`Generator-com-p-current` — output fits the share on, and the share starting up only up to the start-up ramp" @@ -474,16 +515,16 @@ constraints: where: Generator_committable AND Generator_partly_tightened AND position(snapshot) > 0 expression: >- Generator_p - Generator_p_max_pu * Generator_p_nom * Generator_status - + (Generator_p_max_pu * Generator_p_nom - Generator_ramp_limit_start_up * Generator_p_nom) * Generator_start_up <= 0 + + (Generator_p_max_pu * Generator_p_nom - Generator_start_up_rate * Generator_p_nom) * Generator_start_up <= 0 Generator_com_partly_start_up: description: "`Generator-com-partly-start-up` — raising output while a share is starting up is bounded by the ramp of the share on and the start-up ramp of the share coming on" dims: [snapshot, generator] where: Generator_committable AND Generator_partly_tightened expression: >- Generator_p - shift(Generator_p, along=snapshot, offset=1) - - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_limit_up * Generator_p_nom) * Generator_status + - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom) * Generator_status + Generator_p_min_pu * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) - + (Generator_p_min_pu * Generator_p_nom + Generator_ramp_limit_up * Generator_p_nom - Generator_ramp_limit_start_up * Generator_p_nom) + + (Generator_p_min_pu * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom - Generator_start_up_rate * Generator_p_nom) * Generator_start_up <= 0 Generator_com_partly_shut_down: description: "`Generator-com-partly-shut-down` — lowering output while a share is shutting down is bounded likewise, by the shut-down ramp" @@ -491,9 +532,9 @@ constraints: where: Generator_committable AND Generator_partly_tightened expression: >- shift(Generator_p, along=snapshot, offset=1) - Generator_p - - Generator_ramp_limit_shut_down * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) - + (Generator_ramp_limit_shut_down * Generator_p_nom - Generator_ramp_limit_down * Generator_p_nom) * Generator_status - - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_limit_down * Generator_p_nom - Generator_ramp_limit_shut_down * Generator_p_nom) + - Generator_shut_down_rate * Generator_p_nom * shift(Generator_status, along=snapshot, offset=1) + + (Generator_shut_down_rate * Generator_p_nom - Generator_ramp_down_rate * Generator_p_nom) * Generator_status + - (Generator_p_min_pu * Generator_p_nom + Generator_ramp_down_rate * Generator_p_nom - Generator_shut_down_rate * Generator_p_nom) * Generator_start_up <= 0 objective: @@ -507,6 +548,17 @@ objective: + sum(Generator_shut_down * Generator_shut_down_cost) assumptions: + Generator_came_in_running_unless_committable: + holds: "Generator_status_initial == 1" + where: "NOT Generator_committable AND (Generator_ramp_limit_up OR Generator_ramp_limit_down)" + description: >- + PyPSA reads `up_time_before` of a unit that is not committable in its + ramp rows. Where it is zero, PyPSA builds a row at the first snapshot + with nothing carried in, and caps the unit there at zero, or at its + start-up ramp where another unit of the component is committable with a + fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents + the attribute as read only for a committable unit and does not check + it. The spec does not state that row, so it refuses the data Generator_maintenance_events_positive: holds: "Generator_maintenance_events > 0" where: "Generator_maintainable" diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index a057da29..40bfb5c4 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1778,5 +1778,39 @@ "Process-p-ramp_limit_down": 4, "Process-p-ramp_limit_up": 4 } + }, + "rung_47_linearized_ramps": { + "columns": { + "Generator-p": 32, + "Generator-shut_down": 12, + "Generator-start_up": 12, + "Generator-status": 12, + "Link-p": 4 + }, + "global_constraints": {}, + "objective": 12862.5, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 12, + "Generator-com-p-before": 3, + "Generator-com-p-current": 3, + "Generator-com-p-lower": 12, + "Generator-com-p-upper": 12, + "Generator-com-partly-shut-down": 3, + "Generator-com-partly-start-up": 3, + "Generator-com-status-min_up_time_must_stay_up": 0, + "Generator-com-transition-shut-down": 12, + "Generator-com-transition-start-up": 12, + "Generator-fix-p-lower": 20, + "Generator-fix-p-upper": 20, + "Generator-p-ramp_limit_down": 13, + "Generator-p-ramp_limit_up": 10, + "Generator-shut_down-p-fixed-upper": 12, + "Generator-start_up-p-fixed-upper": 12, + "Generator-status-p-fixed-upper": 12, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_47_linearized_ramps.py b/examples/references/pypsa/rung_47_linearized_ramps.py new file mode 100644 index 00000000..1bea7ea6 --- /dev/null +++ b/examples/references/pypsa/rung_47_linearized_ramps.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 47: the relaxed file's ramps and signs — tightening at the full build, a ramp limit per snapshot, a `p_init`, a unit that is not committable, and a sign.""" + +from __future__ import annotations + +import math + +import spine + +MODEL = 'pypsa_linearized_uc.yaml' +OPTIMIZE = {'linearized_unit_commitment': True} + + +def build(): + """The spine plus a relax bus: five units that each carry one of the rows under review, a feeding load and a dear backup.""" + n = spine.build() + n.add('Bus', 'relax') + n.add( + 'Generator', + 'tight47', + bus='relax', + committable=True, + p_nom=50, + p_min_pu=0.2, + marginal_cost=2, + up_time_before=0, + ramp_limit_start_up=0.5, + ramp_limit_shut_down=0.5, + start_up_cost=10, + shut_down_cost=10, + ) + n.add( + 'Generator', + 'steep47', + bus='relax', + committable=True, + p_nom=40, + marginal_cost=3, + start_up_cost=1, + ramp_limit_up=[0.25, 0.25, math.nan, 0.25], + ramp_limit_down=[0.25, 0.25, 0.25, math.nan], + ramp_limit_start_up=0.25, + ) + n.add( + 'Generator', + 'warm47', + bus='relax', + committable=True, + p_nom=40, + marginal_cost=50, + start_up_cost=1, + p_init=40, + ramp_limit_down=0.25, + ramp_limit_shut_down=0.5, + ) + n.add('Generator', 'fixed47', bus='relax', p_nom=60, marginal_cost=1, ramp_limit_up=0.25, ramp_limit_down=0.25) + n.add('Generator', 'sink47', bus='relax', p_nom=20, p_min_pu=0.5, sign=-1) + n.add('Generator', 'backup47', bus='relax', p_nom=300, marginal_cost=500) + n.add('Load', 'feed47', bus='relax', p_set=10, sign=1) + n.add('Load', 'swing47', bus='relax', p_set=[60, 60, 140, 40]) + return n diff --git a/examples/symbols/pypsa_linearized_uc.yaml b/examples/symbols/pypsa_linearized_uc.yaml index a6516c3f..892661eb 100644 --- a/examples/symbols/pypsa_linearized_uc.yaml +++ b/examples/symbols/pypsa_linearized_uc.yaml @@ -57,3 +57,8 @@ names: Generator_maintenance: '\mu' Generator_maintenance_start: '\mu^{\mathrm{up}}' Generator_maintenance_status: '\mu^{u}' + Generator_sign: '\mathrm{sgn}' + Load_sign: '\mathrm{sgn}^{\mathrm{load}}' + Generator_p_init: '\mathrm{p}^{0}' + Generator_ramp_up_allowance: '\Delta^{+}' + Generator_ramp_down_allowance: '\Delta^{-}' From 9270a71f583bf10510d0af8a3bb0a09b14d3c11a Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 12:39:35 +0200 Subject: [PATCH 37/46] docs(pypsa): a start and a stop cost what they cost, with no snapshot or period weight --- docs/examples/pypsa.md | 99 +++++++++++++++++-- examples/pypsa.yaml | 14 +-- examples/references/pypsa/references.json | 24 +++++ .../pypsa/rung_48_unweighted_start_up.py | 44 +++++++++ 4 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 examples/references/pypsa/rung_48_unweighted_start_up.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 02ee344e..5c736dc7 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -607,7 +607,7 @@ def build(): | [`{c}-com-up-time`, `-down-time`](#generator-com-up-time) | done | `sum_back(window=min_up_time)` | | [`{c}-com-status-min_up_time_must_stay_up`](#generator-com-status-min_up_time_must_stay_up) | done | the window is a prep mask — `position()` takes a literal | | [`{c}-com-status-min_down_time_must_stay_up`](#generator-com-status-min_down_time_must_stay_up) | done | the same prep mask over the down time brought in, status zero; PyPSA's name says `_must_stay_up`; rung 24 records it | -| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | | +| [`stand_by_cost`, `start_up_cost`, `shut_down_cost`](#objective) | done | a start and a stop carry no snapshot or period weight, rung 48 | | [`{c}-com-p-before/-current/-partly-*`](pypsa_linearized_uc.md) | done | rungs 12, 44 and 47, a file of its own: Generator commitment on fixed builds | @@ -3834,6 +3834,85 @@ def build(): +### Rung 48 — a start and a stop, unweighted + +`n.optimize(multi_investment_periods=True)` with a committable unit that starts +and stops. PyPSA adds `start_up_cost * start_up` and +`shut_down_cost * shut_down` to the objective without the snapshot's objective +weight and without the period's weight (`optimize.py:414-429`). It weights every +other operating term by both (`optimize.py:262-264`). Over scenarios, it weights +the start and stop costs by the scenario's weight, as every operating term +(`optimize.py:448-452`). The file states the two terms in `scenario_opex` +without a weight, so the scenario weight is the only one they carry. + +The rung builds a committable peaker that starts once and stops once in each of +two periods, with period weights `1.0` and `0.5` and snapshot weights that are +not `1.0`. PyPSA solves to `7325.0`, and to `6525.0` without the start and stop +costs. The difference is `800.0`, the four events at their unweighted cost. The +earlier file weighted them by the period, which reads `600.0` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [`start_up_cost`, `shut_down_cost`](#objective) under `multi_investment_periods` and snapshot weights | done | no snapshot weight and no period weight; the scenario weight only | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `7325.0`, 72 rows. + +
+The network, as PyPSA code + +`rung_48_unweighted_start_up.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 48: a start and a stop cost what they cost — no snapshot weight and no period weight on them, over two weighted periods.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: a committable peaker that starts and stops once in each period.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(3)] + [(2030, datetime(2030, 1, 1, t)) for t in range(3)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 1.5, 2.5] + n.add('Bus', 'grid') + n.add('Generator', 'base48', bus='grid', p_nom=60, marginal_cost=10) + n.add('Generator', 'dear48', bus='grid', p_nom=100, marginal_cost=90) + n.add( + 'Generator', + 'peak48', + bus='grid', + p_nom=50, + marginal_cost=20, + committable=True, + p_min_pu=0.4, + start_up_cost=300, + shut_down_cost=100, + up_time_before=0, + ) + n.add('Load', 'town48', bus='grid', p_set=[50, 100, 50, 50, 100, 50]) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -4241,7 +4320,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{transmission\_volume\_expansion}`$ | `transmission_volume_expansion` over $`\Xi \times \mathcal{I}`$ — what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches | | $`\mathit{transmission\_expansion\_cost}`$ | `transmission_expansion_cost` over $`\Xi \times \mathcal{I}`$ — what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches | | $`\mathit{tech\_capacity\_expansion}`$ | `tech_capacity_expansion` over $`\mathcal{I}`$ — what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set | -| $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight | +| $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight; a start and a stop cost what they cost, unweighted, as PyPSA adds them (`optimize.py:414-429`) | | $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. Like PyPSA, it sums only the components that carry a carrier attribute, so a transformer, which has none, counts in no carrier | | $`\mathrm{r}^{+}`$ | `Carrier_relative_growth` over $`\mathcal{I}`$ — the share of the previous period's additions a carrier's growth limit reads — PyPSA's `max_relative_growth` clipped at zero, so a negative share adds nothing and never tightens the limit | | $`\check{s}`$ | `Line_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the flow a line's post-contingency rows read — its flow where it stands, nothing where it does not, since PyPSA builds those rows for every branch of the sub-network in every snapshot | @@ -9160,7 +9239,7 @@ tech_capacity_expansion: ```yaml scenario_opex: - description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight + description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight; a start and a stop cost what they cost, unweighted, as PyPSA adds them (`optimize.py:414-429`) expression: >- sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_p * Generator_p * Generator_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) @@ -9176,18 +9255,18 @@ scenario_opex: + sum(sum(Store_p * Store_p * Store_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) - + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) - + sum(sum(Generator_shut_down * Generator_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_start_up * Generator_start_up_cost, over=generator), over=snapshot) + + sum(sum(Generator_shut_down * Generator_shut_down_cost, over=generator), over=snapshot) + sum(sum(Link_status * Link_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) - + sum(sum(Link_start_up * Link_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) - + sum(sum(Link_shut_down * Link_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_start_up * Link_start_up_cost, over=link), over=snapshot) + + sum(sum(Link_shut_down * Link_shut_down_cost, over=link), over=snapshot) + sum(sum(Process_status * Process_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) - + sum(sum(Process_start_up * Process_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) - + sum(sum(Process_shut_down * Process_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_start_up * Process_start_up_cost, over=process), over=snapshot) + + sum(sum(Process_shut_down * Process_shut_down_cost, over=process), over=snapshot) ``` ```math -\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{\xi,g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{\xi,g} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{\xi,l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{\xi,l} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{\xi,j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{\xi,j} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{scenario\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{\xi,g} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{\xi,g} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{\xi,l} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{\xi,l} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{\xi,j} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{\xi,j} \qquad \forall\, \xi \in \Xi ``` ### `Carrier_additions` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 70cbeee9..47c77823 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -2061,7 +2061,7 @@ expressions: + sum(Store_e_nom_ext * Store_tech_capacity_weight, over=store) + sum(Process_p_nom_ext * Process_tech_capacity_weight, over=process) scenario_opex: - description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight + description: what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight; a start and a stop cost what they cost, unweighted, as PyPSA adds them (`optimize.py:414-429`) expression: >- sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + sum(sum(Generator_p * Generator_p * Generator_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) @@ -2077,14 +2077,14 @@ expressions: + sum(sum(Store_p * Store_p * Store_marginal_cost_quadratic * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Store_e * Store_marginal_cost_storage * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot) + sum(sum(Generator_status * Generator_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) - + sum(sum(Generator_start_up * Generator_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) - + sum(sum(Generator_shut_down * Generator_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=generator), over=snapshot) + + sum(sum(Generator_start_up * Generator_start_up_cost, over=generator), over=snapshot) + + sum(sum(Generator_shut_down * Generator_shut_down_cost, over=generator), over=snapshot) + sum(sum(Link_status * Link_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) - + sum(sum(Link_start_up * Link_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) - + sum(sum(Link_shut_down * Link_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=link), over=snapshot) + + sum(sum(Link_start_up * Link_start_up_cost, over=link), over=snapshot) + + sum(sum(Link_shut_down * Link_shut_down_cost, over=link), over=snapshot) + sum(sum(Process_status * Process_stand_by_cost * snapshot_weightings_objective * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) - + sum(sum(Process_start_up * Process_start_up_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) - + sum(sum(Process_shut_down * Process_shut_down_cost * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=process), over=snapshot) + + sum(sum(Process_start_up * Process_start_up_cost, over=process), over=snapshot) + + sum(sum(Process_shut_down * Process_shut_down_cost, over=process), over=snapshot) Carrier_additions: description: >- what a carrier adds in a period — every extendable component of that diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 40bfb5c4..afb37f78 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1812,5 +1812,29 @@ "Link-fix-p-lower": 4, "Link-fix-p-upper": 4 } + }, + "rung_48_unweighted_start_up": { + "columns": { + "Generator-p": 18, + "Generator-shut_down": 6, + "Generator-start_up": 6, + "Generator-status": 6 + }, + "global_constraints": {}, + "objective": 7325.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 6, + "Generator-com-p-lower": 6, + "Generator-com-p-upper": 6, + "Generator-com-transition-shut-down": 6, + "Generator-com-transition-start-up": 6, + "Generator-fix-p-lower": 12, + "Generator-fix-p-upper": 12, + "Generator-shut_down-p-fixed-upper": 6, + "Generator-start_up-p-fixed-upper": 6, + "Generator-status-p-fixed-upper": 6 + } } } diff --git a/examples/references/pypsa/rung_48_unweighted_start_up.py b/examples/references/pypsa/rung_48_unweighted_start_up.py new file mode 100644 index 00000000..5ff211ce --- /dev/null +++ b/examples/references/pypsa/rung_48_unweighted_start_up.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 48: a start and a stop cost what they cost — no snapshot weight and no period weight on them, over two weighted periods.""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +OPTIMIZE = {'multi_investment_periods': True} + + +def build(): + """A whole network, not the spine: a committable peaker that starts and stops once in each period.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(3)] + [(2030, datetime(2030, 1, 1, t)) for t in range(3)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 1.5, 2.5] + n.add('Bus', 'grid') + n.add('Generator', 'base48', bus='grid', p_nom=60, marginal_cost=10) + n.add('Generator', 'dear48', bus='grid', p_nom=100, marginal_cost=90) + n.add( + 'Generator', + 'peak48', + bus='grid', + p_nom=50, + marginal_cost=20, + committable=True, + p_min_pu=0.4, + start_up_cost=300, + shut_down_cost=100, + up_time_before=0, + ) + n.add('Load', 'town48', bus='grid', p_set=[50, 100, 50, 50, 100, 50]) + return n From e2410a51b98959a8fdc0949d9082c08427a1c30b Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 12:40:05 +0200 Subject: [PATCH 38/46] docs(pypsa): a growth limit binds only under multi_investment_periods --- docs/examples/pypsa.md | 52 ++++++++++++++++++- examples/pypsa.yaml | 4 +- examples/references/pypsa/references.json | 22 ++++++++ .../pypsa/rung_49_single_period_growth.py | 17 ++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 examples/references/pypsa/rung_49_single_period_growth.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 5c736dc7..83ca4f13 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1381,6 +1381,7 @@ own dimensions. | [`Generator-fix-p-*`, `-ext-p-*`, `-ext-p_nom-*`](#generator-fix-p-lower) | done | rungs 1 and 3, masked by `active` | | [`Carrier-growth_limit`](#carrier-growth_limit) | done | every extendable component of the carrier, counted in the first period a build stands in; `edge=0` at the first period | | [`Carrier-growth_limit`](#carrier-growth_limit) with a negative `max_relative_growth` | done | rung 39 | +| [`Carrier-growth_limit`](#carrier-growth_limit) without `multi_investment_periods` | done | rung 49; not built, so data prep feeds no `max_growth` | | [objective](#objective) | done | period weight on operation; capacity once per period it stands in | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) per period, ramps at period starts | done | rung 29 | | [`StorageUnit-energy_balance`](#storageunit-energy_balance), [`Store-energy_balance`](#store-energy_balance) for storage built in a later period or retired early | done | rung 32 | @@ -3913,6 +3914,55 @@ def build(): +### Rung 49 — a growth limit in one period + +`n.optimize()` with a carrier that carries `max_growth`. PyPSA builds +`Carrier-growth_limit` only under `multi_investment_periods`, and returns +before it reads the carrier otherwise (`global_constraints.py:219-220`). A +single-period run has no growth limit. The file states the row where +`Carrier_max_growth` has a value, so data prep feeds no value on a +single-period run, and no row is built. + +The rung adds a cheap extendable wind unit with `max_growth = 10` to the spine. +PyPSA builds it to `67` and solves to `335.0`, the same as without the limit. +The same network as one investment period under `multi_investment_periods` +caps the build at `10` and solves to `2583.33` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [`Carrier-growth_limit`](#carrier-growth_limit) without `multi_investment_periods` | done | not built; data prep feeds no `Carrier_max_growth` | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `335.0`, 42 rows. + +
+The network, as PyPSA code + +`rung_49_single_period_growth.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 49: a carrier's growth limit without `multi_investment_periods` — PyPSA builds no row, so the build passes the cap.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Carrier', 'wind', max_growth=10) + n.add('Generator', 'wind49', bus='south', carrier='wind', p_nom_extendable=True, p_nom_max=100, capital_cost=5) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -4102,7 +4152,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{new}^{e}`$ | `Store_first_active` over $`\mathcal{Y} \times \mathcal{V}`$ — one in the first period a store stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | | $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | | $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:226-230`), data prep | +| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:226-230`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:219-220`), so data prep feeds no value otherwise | | $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top — the least over the scenarios, as PyPSA takes it, data prep | | $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | | $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — a given flow schedule; a link without one has no row here | diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 47c77823..bf1b550d 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -613,7 +613,9 @@ parameters: description: >- most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it - (`global_constraints.py:226-230`), data prep + (`global_constraints.py:226-230`), data prep. PyPSA reads it only under + `multi_investment_periods` (`global_constraints.py:219-220`), so data + prep feeds no value otherwise dims: [carrier] Carrier_max_relative_growth: description: >- diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index afb37f78..b0135714 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1836,5 +1836,27 @@ "Generator-start_up-p-fixed-upper": 6, "Generator-status-p-fixed-upper": 6 } + }, + "rung_49_single_period_growth": { + "columns": { + "Generator-p": 12, + "Generator-p_nom": 1, + "Link-p": 4 + }, + "global_constraints": {}, + "objective": 335.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-ext-p-lower": 4, + "Generator-ext-p-upper": 4, + "Generator-ext-p_nom-lower": 1, + "Generator-ext-p_nom-upper": 1, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_49_single_period_growth.py b/examples/references/pypsa/rung_49_single_period_growth.py new file mode 100644 index 00000000..f1cb85c2 --- /dev/null +++ b/examples/references/pypsa/rung_49_single_period_growth.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 49: a carrier's growth limit without `multi_investment_periods` — PyPSA builds no row, so the build passes the cap.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Carrier', 'wind', max_growth=10) + n.add('Generator', 'wind49', bus='south', carrier='wind', p_nom_extendable=True, p_nom_max=100, capital_cost=5) + return n From 98e8297a5bb8e6813165aefaae3ed25775097a99 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 12:40:58 +0200 Subject: [PATCH 39/46] docs(pypsa): a load that is not active draws nothing from its bus --- docs/examples/pypsa.md | 79 ++++++++++++++++++- examples/pypsa.yaml | 22 +++++- examples/references/pypsa/references.json | 17 ++++ .../references/pypsa/rung_50_inactive_load.py | 16 ++++ examples/symbols/pypsa.yaml | 2 + 5 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 examples/references/pypsa/rung_50_inactive_load.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 83ca4f13..abeaa29d 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -86,6 +86,7 @@ def build(): | [`Link-fix-p-upper`](#link-fix-p-upper) | done | | | [`Bus-nodal_balance`](#bus-nodal_balance) | done | a loaded bus with nothing attached: PyPSA refuses, see X2 | | [`Bus-nodal_balance`](#bus-nodal_balance) with a component `sign` | done | rung 43 | +| [`Bus-nodal_balance`](#bus-nodal_balance) with a load that is not `active` | done | rung 50; `Load_demand` is zero where `Load_active` is false | | `Bus-meshed-*-nodal_balance` | out | the same balance rows, dealt into linopy containers by how many component columns name a bus — `meshed_thresholds`, an `n.optimize()` keyword defaulting to `[30, 100, 400]`. Same rows, same duals, another name; a modeler whose engine wants the split states it, the file does not (#123) | | [`marginal_cost`](#objective) | done | | | [`marginal_cost_quadratic`](#objective) | done | rungs 10 and 36, below; Generator and Link `p`, Process `p`, StorageUnit `p_dispatch` only, and Store net `p` | @@ -3963,6 +3964,56 @@ def build(): +### Rung 50 — a load that is not active + +`n.optimize()` with a load whose `active` is false. PyPSA masks the load side +of `Bus-nodal_balance` by `active` (`constraints.py:1537-1538`), so an inactive +load draws nothing. A load has no build year and no lifetime, so its `active` is +the static flag alone, in every snapshot and every period +(`descriptors.py:135-136`). PyPSA refuses an `active` that differs by scenario +(`consistency.py:1195`). The file states `Load_active` over the load and +`Load_demand`, the load's signed demand where it is active and zero where it +is not. The balance reads `Load_demand`. A plain run feeds every load active, +and the row collapses to the standard one. + +The rung adds an inactive load to the spine's south bus. PyPSA solves to +`7380.0`, the same as without the load. With the load active, it solves to +`14730.0` (#620). + +| PyPSA | status | note | +| --- | --- | --- | +| [`Bus-nodal_balance`](#bus-nodal_balance) with a load that is not `active` | done | `Load_demand` is zero where `Load_active` is false | + + +> ✔ `pypsa 1.3.0` solves this rung's network at objective `7380.0`, 32 rows. + +
+The network, as PyPSA code + +`rung_50_inactive_load.py` + +```python +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 50: a load that is not `active` — PyPSA drops it from its bus's balance.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Load', 'idle50', bus='south', p_set=[30, 10, 20, 40], active=False) + return n +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -4127,6 +4178,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | | $`\mathrm{sgn}^{\mathrm{load}}`$ | `Load_sign` over $`\mathcal{D}`$ — the sign a load's demand enters its bus's balance with — PyPSA's `sign`, `-1` unless given, `1` for a load that feeds its bus. PyPSA refuses one that differs by scenario (`consistency.py:1187`) | +| $`\mathrm{on}^{\mathrm{load}}`$ | `Load_active` over $`\mathcal{D}`$ — whether a load stands in the model — PyPSA's `active`. A load has no build year and no lifetime, so the flag holds in every snapshot. PyPSA refuses one that differs by scenario (`consistency.py:1195`) | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | | $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation; zero recovers the risk-neutral model | | $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor | @@ -4373,6 +4425,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$ — what a future costs to run — every operating term, weighted by the snapshot's hours and its period, before the scenario's own weight; a start and a stop cost what they cost, unweighted, as PyPSA adds them (`optimize.py:414-429`) | | $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$ — what a carrier adds in a period — every extendable component of that carrier, counting each build in the first period it stands in. Like PyPSA, it sums only the components that carry a carrier attribute, so a transformer, which has none, counts in no carrier | | $`\mathrm{r}^{+}`$ | `Carrier_relative_growth` over $`\mathcal{I}`$ — the share of the previous period's additions a carrier's growth limit reads — PyPSA's `max_relative_growth` clipped at zero, so a negative share adds nothing and never tightens the limit | +| $`\check{\mathrm{load}}`$ | `Load_demand` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — what a load draws from its bus's balance — its demand times its sign where it is active, nothing where it is not, since PyPSA drops an inactive load from the balance (`constraints.py:1537-1538`) | | $`\check{s}`$ | `Line_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the flow a line's post-contingency rows read — its flow where it stands, nothing where it does not, since PyPSA builds those rows for every branch of the sub-network in every snapshot | | $`\check{\sigma}`$ | `Transformer_s_monitored` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — the flow a transformer's post-contingency rows read, as a line's | | $`\hat{s}`$ | `Outage_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}^{\mathrm{out}}`$ — the flow an outage takes off its branch — the outaged line's or transformer's flow before it goes out | @@ -8362,8 +8415,8 @@ Bus_nodal_balance: carrying its flow, meets the load there, less half of every incident line's and transformer's loss — PyPSA dissipates a branch's loss half at either end. Each generator, storage unit, store and load term enters - with its component's `sign` (`constraints.py:1428-1429`, `:1538`). - A bus nothing is attached to has no row; PyPSA refuses one that + with its component's `sign` (`constraints.py:1428-1429`, `:1538`), and + an inactive load not at all. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] expression: >- @@ -8381,11 +8434,11 @@ Bus_nodal_balance: + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus0, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus1, over=transformer, into=bus) - == -sum(Load_sign * Load_p_set, by=Load_bus, over=load, into=bus) + == -sum(Load_demand, by=Load_bus, over=load, into=bus) ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) = -\left( \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \mathrm{sgn}^{\mathrm{load}}_{d} \cdot \mathrm{load}_{\xi,t,d} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) = -\left( \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \check{\mathrm{load}}_{\xi,t,d} \right) \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} ``` ### `Carrier-growth_limit` @@ -9359,6 +9412,24 @@ Carrier_relative_growth: \mathrm{r}^{+}_{i} = \begin{cases} \mathrm{r}_{i} & \text{if } \mathrm{r}_{i} > 0 \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I} ``` +### `Load_demand` + +```yaml +Load_demand: + description: >- + what a load draws from its bus's balance — its demand times its sign + where it is active, nothing where it is not, since PyPSA drops an + inactive load from the balance (`constraints.py:1537-1538`) + dims: [scenario, snapshot, load] + cases: + active: { when: Load_active, expression: Load_sign * Load_p_set } + otherwise: 0 +``` + +```math +\check{\mathrm{load}}_{\xi,t,d} = \begin{cases} \mathrm{sgn}^{\mathrm{load}}_{d} \cdot \mathrm{load}_{\xi,t,d} & \text{if } \mathrm{on}^{\mathrm{load}}_{d} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ d \in \mathcal{D} +``` + ### `Line_s_monitored` ```yaml diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index bf1b550d..184921c1 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -524,6 +524,13 @@ parameters: `sign`, `-1` unless given, `1` for a load that feeds its bus. PyPSA refuses one that differs by scenario (`consistency.py:1187`) dims: [load] + Load_active: + description: >- + whether a load stands in the model — PyPSA's `active`. A load has no + build year and no lifetime, so the flag holds in every snapshot. PyPSA + refuses one that differs by scenario (`consistency.py:1195`) + dims: [load] + dtype: bool scenario_weight: description: PyPSA's `scenario_weightings.weight` — the probability of a future dims: [scenario] @@ -2109,6 +2116,15 @@ expressions: cases: positive: { when: Carrier_max_relative_growth > 0, expression: Carrier_max_relative_growth } otherwise: 0 + Load_demand: + description: >- + what a load draws from its bus's balance — its demand times its sign + where it is active, nothing where it is not, since PyPSA drops an + inactive load from the balance (`constraints.py:1537-1538`) + dims: [scenario, snapshot, load] + cases: + active: { when: Load_active, expression: Load_sign * Load_p_set } + otherwise: 0 Line_s_monitored: description: >- the flow a line's post-contingency rows read — its flow where it stands, @@ -3634,8 +3650,8 @@ constraints: carrying its flow, meets the load there, less half of every incident line's and transformer's loss — PyPSA dissipates a branch's loss half at either end. Each generator, storage unit, store and load term enters - with its component's `sign` (`constraints.py:1428-1429`, `:1538`). - A bus nothing is attached to has no row; PyPSA refuses one that + with its component's `sign` (`constraints.py:1428-1429`, `:1538`), and + an inactive load not at all. A bus nothing is attached to has no row; PyPSA refuses one that carries load, and this file does not yet. dims: [scenario, snapshot, bus] expression: >- @@ -3653,7 +3669,7 @@ constraints: + sum(Transformer_s, by=Transformer_bus1, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus0, over=transformer, into=bus) - 0.5 * sum(Transformer_loss, by=Transformer_bus1, over=transformer, into=bus) - == -sum(Load_sign * Load_p_set, by=Load_bus, over=load, into=bus) + == -sum(Load_demand, by=Load_bus, over=load, into=bus) Carrier_growth_limit: description: >- `Carrier-growth_limit` — what a carrier adds across its extendable components in a period, diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index b0135714..33f78c91 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1858,5 +1858,22 @@ "Link-fix-p-lower": 4, "Link-fix-p-upper": 4 } + }, + "rung_50_inactive_load": { + "columns": { + "Generator-p": 8, + "Link-p": 4 + }, + "global_constraints": {}, + "objective": 7380.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 8, + "Generator-fix-p-lower": 8, + "Generator-fix-p-upper": 8, + "Link-fix-p-lower": 4, + "Link-fix-p-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_50_inactive_load.py b/examples/references/pypsa/rung_50_inactive_load.py new file mode 100644 index 00000000..ab21aa70 --- /dev/null +++ b/examples/references/pypsa/rung_50_inactive_load.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 50: a load that is not `active` — PyPSA drops it from its bus's balance.""" + +from __future__ import annotations + +import spine + + +def build(): + """The spine plus this rung's additions, as a ``pypsa.Network``.""" + n = spine.build() + n.add('Load', 'idle50', bus='south', p_set=[30, 10, 20, 40], active=False) + return n diff --git a/examples/symbols/pypsa.yaml b/examples/symbols/pypsa.yaml index 621c9fd4..d99d2cb3 100644 --- a/examples/symbols/pypsa.yaml +++ b/examples/symbols/pypsa.yaml @@ -107,6 +107,7 @@ names: Link_p_min_pu_nonneg: '\mathrm{nonneg}^{f}' Load_p_set: '\mathrm{load}' Load_sign: '\mathrm{sgn}^{\mathrm{load}}' + Load_active: '\mathrm{on}^{\mathrm{load}}' Generator_p_set: '\mathrm{p}^{\mathrm{set}}' Link_p_set: '\mathrm{f}^{\mathrm{set}}' snapshot_weightings_stores: '\mathrm{w}^{\mathrm{sto}}' @@ -219,6 +220,7 @@ names: Transformer_loss_offset: '\mathrm{b}^{\sigma}' Line_BODF: '\beta' Transformer_BODF: '\beta^{\sigma}' + Load_demand: '\check{\mathrm{load}}' Line_s_monitored: '\check{s}' Transformer_s_monitored: '\check{\sigma}' Outage_s: '\hat{s}' From 9bfb210a5904b2d57d07774a7b7bbb40061f068b Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 12:41:35 +0200 Subject: [PATCH 40/46] docs(pypsa): a security-constrained run dissipates no transmission loss --- docs/examples/pypsa.md | 43 ++++++++++++++++++++++++------------------ examples/pypsa.yaml | 21 ++++++++++++--------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index abeaa29d..a8dad8a7 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -2516,7 +2516,13 @@ its flow within its rating. PyPSA computes the sub-network's branch outage distribution factors (BODF) and copies each flow limit row with the outaged branch's flow, times its factor, added to the left-hand side (`abstract.py:443-489`). The copy keeps the row's sense, right-hand side and -loss term, and its extendable rating. An outage is a line or a transformer, and +extendable rating. It carries no loss term: PyPSA builds the model without +`transmission_losses` and `linearized_unit_commitment` (`abstract.py:437-441`) +and hands any other keyword to the solver (`abstract.py:491`), so a security +run is lossless and integer. Data prep feeds `transmission_losses` false there. +A network with no passive branch is the exception: PyPSA runs a plain +`n.optimize()` with every keyword (`abstract.py:429-435`), and no outage +exists. An outage is a line or a transformer, and a plain list names lines. The outaged branch is monitored too, at the factor `-1`. The file states the copies over an `outage` axis, with the factors as data prep, `Line_BODF` and `Transformer_BODF`. A plain run supplies no outage, @@ -2536,6 +2542,7 @@ bind, in `Transformer-fix-s-lower` against a line outage and in | [`Transformer-fix-s-*-security-…`](#transformer-fix-s-lower-security-for-c-outage-in-sub-network-n), [`Transformer-ext-s-*-security-…`](#transformer-ext-s-lower-security-for-c-outage-in-sub-network-n) | split | the same for a transformer | | a branch not active in a period | done | PyPSA keeps the copy with that branch's flow dropped, so the file reads its flow as zero there; a copy left with no variable is not built here, where linopy counts it; no rung records it | | a security-constrained run over scenarios | out | PyPSA `1.3.0` raises, see [Refusals](#refusals) | +| `transmission_losses`, `linearized_unit_commitment` in a security-constrained run | done | PyPSA builds neither, so the copies carry no loss term and data prep feeds `transmission_losses` false; no rung, since rung 30 is lossless | > ✔ `pypsa 1.3.0` solves this rung's network at objective `22113.333333333332`, 240 rows. @@ -4279,7 +4286,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | | $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1354-1361`) | | $`\beta`$ | `Line_BODF` over $`\mathcal{K} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a line takes on when that branch goes out — PyPSA's `BODF`, from the sub-network's PTDF, data prep; a row only where the line and the outage share a sub-network, -1 at the outaged line itself | -| $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the same; false with no segments is a lossless run | +| $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the same; false with no segments is a lossless run. A security-constrained run over a network with passive branches builds no loss: PyPSA does not hand the keyword to `create_model` (`abstract.py:437-441`) but to the solver (`:491`), so data prep feeds false there | | $`\overline{\ell}`$ | `Line_loss_max` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | | $`\mathrm{a}`$ | `Line_loss_slope` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep | | $`\mathrm{b}`$ | `Line_loss_offset` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | @@ -7376,11 +7383,11 @@ Line_fix_s_lower_security: dimension dims: [scenario, snapshot, line, outage] where: not Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom + expression: Line_s_monitored + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom ``` ```math -\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -7397,11 +7404,11 @@ Line_fix_s_upper_security: states them all over the outage dimension dims: [scenario, snapshot, line, outage] where: not Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom + expression: Line_s_monitored + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom ``` ```math -\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -7419,11 +7426,11 @@ Line_ext_s_lower_security: all over the outage dimension dims: [scenario, snapshot, line, outage] where: Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom_ext + expression: Line_s_monitored + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom_ext ``` ```math -\check{s}_{\xi,t,k} - \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -7441,11 +7448,11 @@ Line_ext_s_upper_security: dimension dims: [scenario, snapshot, line, outage] where: Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom_ext + expression: Line_s_monitored + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom_ext ``` ```math -\check{s}_{\xi,t,k} + \ell_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{k,\kappa} \text{ is defined} ``` ### `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -7463,11 +7470,11 @@ Transformer_fix_s_lower_security: dimension dims: [scenario, snapshot, transformer, outage] where: not Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s_monitored + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom ``` ```math -\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -7484,11 +7491,11 @@ Transformer_fix_s_upper_security: this block states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] where: not Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s_monitored + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom ``` ```math -\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -7506,11 +7513,11 @@ Transformer_ext_s_lower_security: all over the outage dimension dims: [scenario, snapshot, transformer, outage] where: Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s_monitored + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext ``` ```math -\check{\sigma}_{\xi,t,m} - \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -7528,11 +7535,11 @@ Transformer_ext_s_upper_security: outage dimension dims: [scenario, snapshot, transformer, outage] where: Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s_monitored + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext ``` ```math -\check{\sigma}_{\xi,t,m} + \ell^{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{m,\kappa} \text{ is defined} ``` ### `Kirchhoff-Voltage-Law` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 184921c1..2aab8031 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -905,7 +905,10 @@ parameters: whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the - same; false with no segments is a lossless run + same; false with no segments is a lossless run. A security-constrained + run over a network with passive branches builds no loss: PyPSA does not + hand the keyword to `create_model` (`abstract.py:437-441`) but to the + solver (`:491`), so data prep feeds false there dims: [] dtype: bool Line_loss_max: @@ -3238,7 +3241,7 @@ constraints: dimension dims: [scenario, snapshot, line, outage] where: not Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom + expression: Line_s_monitored + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom Line_fix_s_upper_security: description: >- `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -3248,7 +3251,7 @@ constraints: states them all over the outage dimension dims: [scenario, snapshot, line, outage] where: not Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom + expression: Line_s_monitored + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom Line_ext_s_lower_security: description: >- `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` — @@ -3259,7 +3262,7 @@ constraints: all over the outage dimension dims: [scenario, snapshot, line, outage] where: Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored - Line_loss + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom_ext + expression: Line_s_monitored + Line_BODF * Outage_s >= -Line_s_max_pu * Line_s_nom_ext Line_ext_s_upper_security: description: >- `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -3270,7 +3273,7 @@ constraints: dimension dims: [scenario, snapshot, line, outage] where: Line_s_nom_extendable AND Line_BODF - expression: Line_s_monitored + Line_loss + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom_ext + expression: Line_s_monitored + Line_BODF * Outage_s <= Line_s_max_pu * Line_s_nom_ext Transformer_fix_s_lower_security: description: >- `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -3281,7 +3284,7 @@ constraints: dimension dims: [scenario, snapshot, transformer, outage] where: not Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s_monitored + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom Transformer_fix_s_upper_security: description: >- `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -3291,7 +3294,7 @@ constraints: this block states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] where: not Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom + expression: Transformer_s_monitored + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom Transformer_ext_s_lower_security: description: >- `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -3302,7 +3305,7 @@ constraints: all over the outage dimension dims: [scenario, snapshot, transformer, outage] where: Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored - Transformer_loss + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s_monitored + Transformer_BODF * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext Transformer_ext_s_upper_security: description: >- `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -3313,7 +3316,7 @@ constraints: outage dimension dims: [scenario, snapshot, transformer, outage] where: Transformer_s_nom_extendable AND Transformer_BODF - expression: Transformer_s_monitored + Transformer_loss + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext + expression: Transformer_s_monitored + Transformer_BODF * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext Kirchhoff_Voltage_Law: description: >- `Kirchhoff-Voltage-Law` — around every independent cycle the From 5bb13366676865b185421c8ab6fbdb1d770181bc Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 12:53:17 +0200 Subject: [PATCH 41/46] docs: the changelog lists the pypsa spec that covers every model class and component --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4dfa965..de477a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp ## Upcoming version +- docs(pypsa): a single spec covers every pypsa model class and component ([#620](https://github.com/energy-models/mathspec/pull/620)) + ## 0.1.0rc1 (2026-09-25) The release candidate for 0.1.0, the first release on PyPI: From 348d925d675f2cc5e83f8a95f420c614fb2d74e9 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 13:34:53 +0200 Subject: [PATCH 42/46] 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/examples/pypsa.md | 124 ++++++++++++++++-- examples/pypsa.yaml | 46 +++++-- examples/references/pypsa/reference.py | 72 ++++++++-- examples/references/pypsa/references.json | 24 ++++ .../pypsa/rung_51_growth_retired_asset.py | 64 +++++++++ tests/test_pypsa_references.py | 28 +++- tools/gallery.py | 20 ++- 7 files changed, 337 insertions(+), 41 deletions(-) create mode 100644 examples/references/pypsa/rung_51_growth_retired_asset.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 09618db0..8d9c8cd1 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -24,11 +24,15 @@ A row is **done** once the file states it as the one block PyPSA builds. **split** means the same feasible region and optimum under a different statement, such as several `where:` blocks. **open** means not stated yet. **out** means never stated, deliberately: emitted only under the keyword, -scope or version the note names. A name carrying `{k}`, `{s}`, `{c}` or `{n}` stands +scope or version the note names. **diverges** means the file states the +intended math where PyPSA `1.3.0` has a bug; the note names the issue, and the +rung records the intended objective and what PyPSA gives until the fix ships. A name carrying `{k}`, `{s}`, `{c}` or `{n}` stands for the family PyPSA numbers per segment, scenario, outaged component or sub-network. -Each rung's banner states what PyPSA solved its reference network to. +Each rung's banner states what PyPSA solved its reference network to. A +rung that records a PyPSA bug, marked ✘, states what PyPSA gives beside the +intended objective. > Every rung's network is `spine.build()` plus the rung's own `n.add` calls, data inline; a keyword not passed is PyPSA's default. A banner states what PyPSA solved the rung to; how an engine attaches the network to the file, and what it makes of it, is that engine's own record. @@ -4021,6 +4025,106 @@ def build(): +### Rung 51 — a growth limit after an asset retires + +`n.optimize(multi_investment_periods=True)` with a carrier that carries +`max_growth`, and an extendable asset of that carrier that retires before the +last period. The file counts a build in the first period it stands in only: +`{c}_first_active` is one there and zero elsewhere. PyPSA `1.3.0` takes +`active.cumsum() == 1`, which stays true after the asset retires, so it counts +the asset again in every later period (`global_constraints.py:276`, +[PyPSA/PyPSA#1938](https://github.com/PyPSA/PyPSA/issues/1938)). + +The rung builds two solar units under one carrier with `max_growth = 10`. The +old one stands in 2020 only, the new one in 2030 only. Both build to `10` in +the file. PyPSA holds the new one at `10` minus the old one's build, so it +builds nothing in 2030 and solves to `5185.0`. The oracle is the same network +with a carrier per unit, each with the same limit: each carrier has one asset, +and the repeated row PyPSA builds for the old one repeats its own bound. It +solves to `3432.5`. Without `max_growth`, the network solves to `248.75`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Carrier-growth_limit`](#carrier-growth_limit) with an asset that retires | diverges | [PyPSA/PyPSA#1938](https://github.com/PyPSA/PyPSA/issues/1938); `{c}_first_active` is zero after the first period an asset stands in | + + +> ✘ `pypsa 1.3.0` solves this rung's network at objective `5185.0`, 26 rows, [PyPSA/PyPSA#1938](https://github.com/PyPSA/PyPSA/issues/1938). The intended objective is `3432.5`. + +
+The network, as PyPSA code + +`rung_51_growth_retired_asset.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 51: a carrier's growth limit counts an asset in the first period it stands in only, not again after it retires. + +PyPSA 1.3.0 counts an asset that retires in every later period too (PyPSA/PyPSA#1938). +The oracle gives each build its own carrier with the same limit: each carrier +then has one asset, which PyPSA counts in its first period, and a retired one +counted again repeats a row it already has. +""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +ISSUE = 1938 +OPTIMIZE = {'multi_investment_periods': True} + + +def network(carriers: dict[str, str]): + """Two periods, a solar unit that stands in 2020 only and one built in 2030, each under the carrier named for it.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', 'grid') + n.add('Carrier', 'gas') + for carrier in sorted(set(carriers.values())): + n.add('Carrier', carrier, max_growth=10) + for name, build_year in (('solar_old', 2020), ('solar_new', 2030)): + n.add( + 'Generator', + name, + bus='grid', + carrier=carriers[name], + p_nom_extendable=True, + p_nom_max=50, + marginal_cost=1, + capital_cost=5, + build_year=build_year, + lifetime=10, + ) + n.add('Generator', 'backup', bus='grid', carrier='gas', p_nom=100, marginal_cost=80) + n.add('Load', 'town', bus='grid', p_set=[15, 20, 15, 20]) + return n + + +def build(): + """Both solar units under one carrier with `max_growth = 10`; the old one retires after 2020.""" + return network({'solar_old': 'solar', 'solar_new': 'solar'}) + + +def oracle(): + """The same network with a carrier per build: PyPSA counts each build in its first period only.""" + return [(1.0, network({'solar_old': 'solar20', 'solar_new': 'solar30'}))] +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -4205,12 +4309,12 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{W}^{s}`$ | `Line_capital_weight` over $`\mathcal{K}`$ — the sum of period weights a line stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{W}^{z}`$ | `Process_capital_weight` over $`\mathcal{J}`$ — the sum of period weights a process stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{W}^{\sigma}`$ | `Transformer_capital_weight` over $`\mathcal{M}`$ — the sum of period weights a transformer stands in — PyPSA's `active * period_weighting`, summed, data prep | -| $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\mathrm{new}^{f}`$ | `Link_first_active` over $`\mathcal{Y} \times \mathcal{L}`$ — one in the first period a link stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\mathrm{new}^{h}`$ | `StorageUnit_first_active` over $`\mathcal{Y} \times \mathcal{S}`$ — one in the first period a storage unit stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\mathrm{new}^{e}`$ | `Store_first_active` over $`\mathcal{Y} \times \mathcal{V}`$ — one in the first period a store stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | -| $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep | +| $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere, data prep. PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a generator that has retired in every later period (`global_constraints.py:276`, PyPSA/PyPSA\#1938) | +| $`\mathrm{new}^{f}`$ | `Link_first_active` over $`\mathcal{Y} \times \mathcal{L}`$ — one in the first period a link stands in, zero elsewhere, data prep. PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a link that has retired in every later period (`global_constraints.py:276`, PyPSA/PyPSA\#1938) | +| $`\mathrm{new}^{h}`$ | `StorageUnit_first_active` over $`\mathcal{Y} \times \mathcal{S}`$ — one in the first period a storage unit stands in, zero elsewhere, data prep. PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a storage unit that has retired in every later period (`global_constraints.py:276`, PyPSA/PyPSA\#1938) | +| $`\mathrm{new}^{e}`$ | `Store_first_active` over $`\mathcal{Y} \times \mathcal{V}`$ — one in the first period a store stands in, zero elsewhere, data prep. PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a store that has retired in every later period (`global_constraints.py:276`, PyPSA/PyPSA\#1938) | +| $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere, data prep. PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a line that has retired in every later period (`global_constraints.py:276`, PyPSA/PyPSA\#1938) | +| $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere, data prep. PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a process that has retired in every later period (`global_constraints.py:276`, PyPSA/PyPSA\#1938) | | $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:226-230`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:219-220`), so data prep feeds no value otherwise | | $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top — the least over the scenarios, as PyPSA takes it, data prep | | $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | @@ -4249,7 +4353,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{cyc}`$ | `StorageUnit_cyclic_state_of_charge` over $`\Xi \times \mathcal{S}`$ — whether the horizon closes on itself instead of opening on the initial charge | | $`\mathrm{cyc}^{y}`$ | `StorageUnit_cyclic_state_of_charge_per_period` over $`\Xi \times \mathcal{S}`$ — whether each investment period closes on itself instead of carrying its charge on to the next; it overrides `cyclic_state_of_charge` and `state_of_charge_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{reset}`$ | `StorageUnit_state_of_charge_initial_per_period` over $`\Xi \times \mathcal{S}`$ — whether each investment period opens on the initial charge instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | -| $`\mathrm{open}`$ | `StorageUnit_opens_late` over $`\mathcal{T} \times \mathcal{S}`$ — whether a snapshot is the first a storage unit stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every unit stands throughout | +| $`\mathrm{open}`$ | `StorageUnit_opens_late` over $`\mathcal{T} \times \mathcal{S}`$ — whether a snapshot is the first a storage unit stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` over the snapshots it stands in, past the first snapshot, data prep; false in a run where every unit stands throughout | | $`\mathrm{idle}`$ | `StorageUnit_inactive_snapshots` over $`\mathcal{S}`$ — how many snapshots a storage unit does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic unit reaches back this many snapshots further, so it closes on the last snapshot it stands in | | $`\mathrm{c}^{h}`$ | `StorageUnit_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of dispatch | | $`\mathrm{c}^{h,(2)}`$ | `StorageUnit_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of the square of one unit of dispatch; storing is not charged | @@ -4269,7 +4373,7 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{cyc}^{e}`$ | `Store_e_cyclic` over $`\Xi \times \mathcal{V}`$ — whether the horizon closes on itself instead of opening on the initial energy | | $`\mathrm{cyc}^{e,y}`$ | `Store_e_cyclic_per_period` over $`\Xi \times \mathcal{V}`$ — whether each investment period closes on itself instead of carrying its energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`. PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | | $`\mathrm{reset}^{e}`$ | `Store_e_initial_per_period` over $`\Xi \times \mathcal{V}`$ — whether each investment period opens on the initial energy instead of carrying the previous period's; PyPSA reads it only under `multi_investment_periods`, so data prep feeds false otherwise | -| $`\mathrm{open}^{e}`$ | `Store_opens_late` over $`\mathcal{T} \times \mathcal{V}`$ — whether a snapshot is the first a store stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` past the first snapshot, data prep; false in a run where every store stands throughout | +| $`\mathrm{open}^{e}`$ | `Store_opens_late` over $`\mathcal{T} \times \mathcal{V}`$ — whether a snapshot is the first a store stands in, where that is not the first of the horizon — PyPSA's `active.cumsum() == 1` over the snapshots it stands in, past the first snapshot, data prep; false in a run where every store stands throughout | | $`\mathrm{idle}^{e}`$ | `Store_inactive_snapshots` over $`\mathcal{V}`$ — how many snapshots a store does not stand in — PyPSA's `(~active).sum()`, data prep. A cyclic store reaches back this many snapshots further, so it closes on the last snapshot it stands in | | $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | | $`\mathrm{c}^{q,(2)}`$ | `Store_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of the square of the net power delivered, so charging costs as much as delivering | diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index f58e2ff8..55a73a03 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -599,22 +599,46 @@ parameters: description: the sum of period weights a transformer stands in — PyPSA's `active * period_weighting`, summed, data prep dims: [transformer] Generator_first_active: - description: one in the first period a generator stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + description: >- + one in the first period a generator stands in, zero elsewhere, data prep. + PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a generator + that has retired in every later period (`global_constraints.py:276`, + PyPSA/PyPSA#1938) dims: [period, generator] Link_first_active: - description: one in the first period a link stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + description: >- + one in the first period a link stands in, zero elsewhere, data prep. + PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a link + that has retired in every later period (`global_constraints.py:276`, + PyPSA/PyPSA#1938) dims: [period, link] StorageUnit_first_active: - description: one in the first period a storage unit stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + description: >- + one in the first period a storage unit stands in, zero elsewhere, data prep. + PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a storage unit + that has retired in every later period (`global_constraints.py:276`, + PyPSA/PyPSA#1938) dims: [period, storage_unit] Store_first_active: - description: one in the first period a store stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + description: >- + one in the first period a store stands in, zero elsewhere, data prep. + PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a store + that has retired in every later period (`global_constraints.py:276`, + PyPSA/PyPSA#1938) dims: [period, store] Line_first_active: - description: one in the first period a line stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + description: >- + one in the first period a line stands in, zero elsewhere, data prep. + PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a line + that has retired in every later period (`global_constraints.py:276`, + PyPSA/PyPSA#1938) dims: [period, line] Process_first_active: - description: one in the first period a process stands in, zero elsewhere — PyPSA's `active.cumsum() == 1`, data prep + description: >- + one in the first period a process stands in, zero elsewhere, data prep. + PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a process + that has retired in every later period (`global_constraints.py:276`, + PyPSA/PyPSA#1938) dims: [period, process] Carrier_max_growth: description: >- @@ -754,8 +778,9 @@ parameters: StorageUnit_opens_late: description: >- whether a snapshot is the first a storage unit stands in, where that is - not the first of the horizon — PyPSA's `active.cumsum() == 1` past the - first snapshot, data prep; false in a run where every unit stands throughout + not the first of the horizon — PyPSA's `active.cumsum() == 1` over the + snapshots it stands in, past the first snapshot, data prep; false in a + run where every unit stands throughout dims: [snapshot, storage_unit] dtype: bool StorageUnit_inactive_snapshots: @@ -836,8 +861,9 @@ parameters: Store_opens_late: description: >- whether a snapshot is the first a store stands in, where that is not the - first of the horizon — PyPSA's `active.cumsum() == 1` past the first - snapshot, data prep; false in a run where every store stands throughout + first of the horizon — PyPSA's `active.cumsum() == 1` over the snapshots + it stands in, past the first snapshot, data prep; false in a run where + every store stands throughout dims: [snapshot, store] dtype: bool Store_inactive_snapshots: diff --git a/examples/references/pypsa/reference.py b/examples/references/pypsa/reference.py index bd5f49a0..ed1e0194 100644 --- a/examples/references/pypsa/reference.py +++ b/examples/references/pypsa/reference.py @@ -17,7 +17,15 @@ the PyPSA model under review is the script itself. A rung stated by a file of its own names it as `MODEL`; one that needs `n.optimize` keywords names them as `OPTIMIZE`; one that names `BRANCH_OUTAGES` is solved by -`n.optimize.optimize_security_constrained` over them. PyPSA is not a dependency +`n.optimize.optimize_security_constrained` over them. + +A rung that names `ISSUE` records a PyPSA bug: the network PyPSA 1.3.0 gets +wrong, until the fix ships. Its `oracle()` returns networks PyPSA solves +correctly, each with the weight its objective takes in the intended one. The +record's `objective` is that intended objective, and `diverges` holds the issue +and what PyPSA gives instead: its objective, or the exception it raises. Rows +and columns are what PyPSA built, where it built any. `--check` fails where +PyPSA no longer diverges, and names the issue. PyPSA is not a dependency of this project; this script pins the versions the recorded numbers are from, and the `PyPSA references` workflow runs `--check` on every change. """ @@ -66,8 +74,7 @@ def record(n: pypsa.Network) -> dict[str, object]: } -def solved(rung: str) -> dict[str, object]: - n = build(rung) +def optimized(rung: str, n: pypsa.Network) -> pypsa.Network: outages = getattr(importlib.import_module(rung), 'BRANCH_OUTAGES', None) if outages is None: status, condition = n.optimize(solver_name='highs', **keywords(rung)) @@ -76,7 +83,49 @@ def solved(rung: str) -> dict[str, object]: solver_name='highs', branch_outages=outages, **keywords(rung) ) assert status == 'ok', f'{rung}: HiGHS did not solve — {status} / {condition}' - return record(n) + return n + + +def close(a: float, b: float) -> bool: + return math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-6) + + +def diverged(rung: str, issue: int) -> dict[str, object]: + """The intended objective from the rung's oracle, beside what PyPSA gives on the rung's own network.""" + intended = sum(weight * float(optimized(rung, n).objective) for weight, n in importlib.import_module(rung).oracle()) + try: + seen = record(optimized(rung, build(rung))) + except Exception as error: + return { + 'pypsa': pypsa.__version__, + 'objective': intended, + 'diverges': {'issue': issue, 'raises': type(error).__name__}, + } + return {**seen, 'objective': intended, 'diverges': {'issue': issue, 'objective': seen['objective']}} + + +def solved(rung: str) -> dict[str, object]: + issue = getattr(importlib.import_module(rung), 'ISSUE', None) + return record(optimized(rung, build(rung))) if issue is None else diverged(rung, issue) + + +def fixed(seen: dict[str, object]) -> bool: + """Whether PyPSA gives the intended objective on a rung that records one of its bugs.""" + diverges = seen.get('diverges', {}) + return 'objective' in diverges and close(diverges['objective'], seen['objective']) + + +def matches(kept: dict[str, object], seen: dict[str, object]) -> bool: + """Equal records, every objective compared to a tolerance.""" + + def split(r: dict[str, object]) -> tuple[list[float], dict[str, object]]: + diverges = r.get('diverges', {}) + numbers = [r['objective'], *([diverges['objective']] if 'objective' in diverges else [])] + rest = {k: v for k, v in diverges.items() if k != 'objective'} + return numbers, {**r, 'objective': None, 'diverges': rest} + + (a, rest_a), (b, rest_b) = split(kept), split(seen) + return rest_a == rest_b and len(a) == len(b) and all(map(close, a, b)) def main(argv: list[str]) -> int: @@ -86,14 +135,15 @@ def main(argv: list[str]) -> int: for rung in rungs(): seen = solved(rung) print(f'{rung}: objective {seen["objective"]}') + if fixed(seen): + issue = seen['diverges']['issue'] + print( + f'{rung}: pypsa {pypsa.__version__} solves PyPSA/PyPSA#{issue} as intended — drop the divergence', + file=sys.stderr, + ) + return 1 if check: - kept = stamped.get(rung) - if ( - kept is None - or not math.isclose(kept['objective'], seen['objective'], rel_tol=1e-9, abs_tol=1e-6) - or {k: v for k, v in kept.items() if k != 'objective'} - != {k: v for k, v in seen.items() if k != 'objective'} - ): + if (kept := stamped.get(rung)) is None or not matches(kept, seen): stale.append(rung) else: stamped[rung] = seen diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 33f78c91..13de643e 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1875,5 +1875,29 @@ "Link-fix-p-lower": 4, "Link-fix-p-upper": 4 } + }, + "rung_51_growth_retired_asset": { + "columns": { + "Generator-p": 8, + "Generator-p_nom": 2 + }, + "diverges": { + "issue": 1938, + "objective": 5185.0 + }, + "global_constraints": {}, + "objective": 3432.5, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 4, + "Carrier-growth_limit": 2, + "Generator-ext-p-lower": 4, + "Generator-ext-p-upper": 4, + "Generator-ext-p_nom-lower": 2, + "Generator-ext-p_nom-upper": 2, + "Generator-fix-p-lower": 4, + "Generator-fix-p-upper": 4 + } } } diff --git a/examples/references/pypsa/rung_51_growth_retired_asset.py b/examples/references/pypsa/rung_51_growth_retired_asset.py new file mode 100644 index 00000000..2417a27d --- /dev/null +++ b/examples/references/pypsa/rung_51_growth_retired_asset.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 51: a carrier's growth limit counts an asset in the first period it stands in only, not again after it retires. + +PyPSA 1.3.0 counts an asset that retires in every later period too (PyPSA/PyPSA#1938). +The oracle gives each build its own carrier with the same limit: each carrier +then has one asset, which PyPSA counts in its first period, and a retired one +counted again repeats a row it already has. +""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +ISSUE = 1938 +OPTIMIZE = {'multi_investment_periods': True} + + +def network(carriers: dict[str, str]): + """Two periods, a solar unit that stands in 2020 only and one built in 2030, each under the carrier named for it.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', 'grid') + n.add('Carrier', 'gas') + for carrier in sorted(set(carriers.values())): + n.add('Carrier', carrier, max_growth=10) + for name, build_year in (('solar_old', 2020), ('solar_new', 2030)): + n.add( + 'Generator', + name, + bus='grid', + carrier=carriers[name], + p_nom_extendable=True, + p_nom_max=50, + marginal_cost=1, + capital_cost=5, + build_year=build_year, + lifetime=10, + ) + n.add('Generator', 'backup', bus='grid', carrier='gas', p_nom=100, marginal_cost=80) + n.add('Load', 'town', bus='grid', p_set=[15, 20, 15, 20]) + return n + + +def build(): + """Both solar units under one carrier with `max_growth = 10`; the old one retires after 2020.""" + return network({'solar_old': 'solar', 'solar_new': 'solar'}) + + +def oracle(): + """The same network with a carrier per build: PyPSA counts each build in its first period only.""" + return [(1.0, network({'solar_old': 'solar20', 'solar_new': 'solar30'}))] diff --git a/tests/test_pypsa_references.py b/tests/test_pypsa_references.py index 17954f36..96621168 100644 --- a/tests/test_pypsa_references.py +++ b/tests/test_pypsa_references.py @@ -38,11 +38,12 @@ #: those rows after each row's own label, so they are matched through the #: recorded type and sense instead of by name. GC_TYPES = {name for name in ROWS_DECLARED if not name[0].isupper()} -RECORDED_ROWS: set[str] = set().union(*(record['rows'] for record in RECORDED.values())) -RECORDED_COLUMNS: set[str] = set().union(*(record['columns'] for record in RECORDED.values())) -GC_RECORDED: dict[str, dict] = { - label: gc for record in RECORDED.values() for label, gc in record['global_constraints'].items() -} +#: A rung that records a PyPSA bug PyPSA raises on has no rows, columns or global constraints of its own. +BUILT = [record for record in RECORDED.values() if 'raises' not in record.get('diverges', {})] +RECORDED_ROWS: set[str] = set().union(*(record['rows'] for record in BUILT)) +RECORDED_COLUMNS: set[str] = set().union(*(record['columns'] for record in BUILT)) +GC_RECORDED: dict[str, dict] = {label: gc for record in BUILT for label, gc in record['global_constraints'].items()} +DIVERGED = {stem: record for stem, record in RECORDED.items() if 'diverges' in record} @pytest.mark.parametrize('key', ['spine', *RUNGS]) @@ -95,7 +96,22 @@ def test_the_record_is_from_the_pinned_pypsa(stem: str): def test_the_recorded_solve_is_usable_as_an_oracle(stem: str): recorded = RECORDED[stem] assert math.isfinite(recorded['objective']), 'an oracle needs a finite objective' - assert recorded['rows'], 'an oracle needs the row counts an engine would compare' + if recorded in BUILT: + assert recorded['rows'], 'an oracle needs the row counts an engine would compare' + + +@pytest.mark.parametrize('stem', sorted(DIVERGED), ids=sorted(DIVERGED)) +def test_a_divergence_names_its_issue_and_what_pypsa_gives_instead(stem: str): + recorded = RECORDED[stem] + diverges = recorded['diverges'] + module = (REFERENCES / f'{stem}.py').read_text() + assert f'ISSUE = {diverges["issue"]}' in module, 'the record names the issue its rung script names' + assert any(f'PyPSA/PyPSA#{diverges["issue"]}' in text for text in PAGE_TEXTS), ( + 'the page links the issue a rung records' + ) + assert 'raises' in diverges or not math.isclose(diverges['objective'], recorded['objective'], rel_tol=1e-9), ( + 'a divergence records what PyPSA gives instead of the intended objective: an exception or another objective' + ) def test_pypsa_builds_no_variable_the_files_do_not_declare(): diff --git a/tools/gallery.py b/tools/gallery.py index f5879f1a..bf6c1d54 100644 --- a/tools/gallery.py +++ b/tools/gallery.py @@ -144,13 +144,25 @@ def _script(name: str) -> str: return f'`{name}.py`\n\n```python\n{(REFERENCES / f"{name}.py").read_text().strip()}\n```' +def _banner(recorded: dict) -> str: + """What PyPSA solved the rung to; for a rung that records a PyPSA bug, also what the file intends and the issue.""" + pypsa = f'`pypsa {recorded["pypsa"]}`' + if 'diverges' not in recorded: + return f"> ✔ {pypsa} solves this rung's network at objective `{recorded['objective']}`, {sum(recorded['rows'].values())} rows." + diverges = recorded['diverges'] + issue = f'[PyPSA/PyPSA#{diverges["issue"]}](https://github.com/PyPSA/PyPSA/issues/{diverges["issue"]})' + gives = ( + f"raises `{diverges['raises']}` on this rung's network" + if 'raises' in diverges + else f"solves this rung's network at objective `{diverges['objective']}`, {sum(recorded['rows'].values())} rows" + ) + return f'> ✘ {pypsa} {gives}, {issue}. The intended objective is `{recorded["objective"]}`.' + + def reference_block(stem: str) -> str: """A rung's oracle: the recorded solve, then the PyPSA script that builds its network.""" - recorded = RECORDED[stem] - rows = sum(recorded['rows'].values()) return ( - f"> ✔ `pypsa {recorded['pypsa']}` solves this rung's network at objective " - f'`{recorded["objective"]}`, {rows} rows.\n' + f'{_banner(RECORDED[stem])}\n' '\n' '
\n' 'The network, as PyPSA code\n' From 80a378e66a339786608d25e470c1d82db3efaa5a Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 13:35:16 +0200 Subject: [PATCH 43/46] 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/examples/pypsa.md | 197 +++++++++++++++++- examples/references/pypsa/references.json | 68 ++++++ .../pypsa/rung_52_scenario_cost_limit.py | 54 +++++ .../rung_53_scenario_period_volume_limit.py | 73 +++++++ 4 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 examples/references/pypsa/rung_52_scenario_cost_limit.py create mode 100644 examples/references/pypsa/rung_53_scenario_period_volume_limit.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 8d9c8cd1..f50aaffb 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -3389,8 +3389,8 @@ network and the calm values in both futures, PyPSA solves to | PyPSA | status | note | | --- | --- | --- | | [`primary_energy`](#primary_energy), [`operational_limit`](#operational_limit), [`transmission_volume_expansion_limit`](#transmission_volume_expansion_limit) with a constant and sense per scenario | done | `GlobalConstraint_constant` and `GlobalConstraint_sense` over `scenario` | -| `transmission_expansion_cost_limit` on a network with scenarios | out | PyPSA `1.3.0` builds no row: it matches extendable names against a table indexed by scenario and name, and finds none (`global_constraints.py:916`). The file builds the row per scenario | -| `transmission_volume_expansion_limit` on a network with scenarios and `multi_investment_periods` | out | PyPSA `1.3.0` builds no row: the active-asset filter reindexes a table indexed by scenario and name by the names alone, and keeps none (`global_constraints.py:828`, `descriptors.py:261-263`). The file builds the row per scenario | +| `transmission_expansion_cost_limit` on a network with scenarios | diverges | rung 52, [PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939) | +| `transmission_volume_expansion_limit` on a network with scenarios and `multi_investment_periods` | diverges | rung 53, [PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939) | | a `carrier_attribute` or `investment_period` per scenario | done | PyPSA reads both per scenario (`global_constraints.py:797-802`); the weights and `GlobalConstraint_counts_snapshot` span `scenario` | @@ -4125,6 +4125,199 @@ def oracle():
+### Rung 52 — a transmission cost limit per scenario + +`n.set_scenarios(...)` with a `transmission_expansion_cost_limit` row. The +file builds the row in every scenario, as it builds every global constraint. +PyPSA `1.3.0` builds no row: it matches the extendable names against a table +indexed by scenario and name, and finds none (`global_constraints.py:916`, +[PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939)). + +The rung adds an extendable DC link to the spine under a cost limit of `150`, +so the link builds `15`. The two futures are identical. The oracle is the same +network without scenarios, which PyPSA solves with the row to `15630.0`. With +scenarios, PyPSA solves to `11890.0`, the objective of the network without the +row. + +| PyPSA | status | note | +| --- | --- | --- | +| [`transmission_expansion_cost_limit`](#transmission_expansion_cost_limit) on a network with scenarios | diverges | [PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939); the row per scenario | + + +> ✘ `pypsa 1.3.0` solves this rung's network at objective `11890.0`, 84 rows, [PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939). The intended objective is `15630.0`. + +
+The network, as PyPSA code + +`rung_52_scenario_cost_limit.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 52: a `transmission_expansion_cost_limit` row holds in every scenario. + +PyPSA 1.3.0 builds no such row on a network with scenarios (PyPSA/PyPSA#1939). +The two futures are identical, so the oracle is the same network without +scenarios, which PyPSA solves with the row. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1939 + + +def network(): + """The spine plus an extendable DC link whose build a cost limit of 150 caps.""" + n = spine.build() + n.add('Carrier', 'DC') + n.add( + 'Link', + 'hvdc52', + bus0='north', + bus1='south', + carrier='DC', + p_nom_extendable=True, + p_nom_max=100, + capital_cost=10, + ) + n.add('Load', 'port52', bus='south', p_set=40) + n.add( + 'GlobalConstraint', + 'cost52', + type='transmission_expansion_cost_limit', + carrier_attribute='DC', + sense='<=', + constant=150, + ) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] +``` + +
+ + +### Rung 53 — a transmission volume limit per scenario and period + +`n.set_scenarios(...)` and `n.optimize(multi_investment_periods=True)` with a +`transmission_volume_expansion_limit` row. The file builds the row in every +scenario. PyPSA `1.3.0` builds no row: the active-asset filter reindexes a table +indexed by scenario and name by the names alone, and keeps none +(`global_constraints.py:828`, `descriptors.py:263`, +[PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939)). Without +periods, PyPSA builds the row (rung 40). + +The rung is two periods with an extendable line of length `3` under a volume +limit of `60`, so the line builds `20`. The two futures are identical. The +oracle is the same network without scenarios, which PyPSA solves with the row to +`15005.0`. With scenarios, PyPSA solves to `1465.0`, the objective of the +network without the row. + +| PyPSA | status | note | +| --- | --- | --- | +| [`transmission_volume_expansion_limit`](#transmission_volume_expansion_limit) on a network with scenarios and `multi_investment_periods` | diverges | [PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939); the row per scenario | + + +> ✘ `pypsa 1.3.0` solves this rung's network at objective `1465.0`, 68 rows, [PyPSA/PyPSA#1939](https://github.com/PyPSA/PyPSA/issues/1939). The intended objective is `15005.0`. + +
+The network, as PyPSA code + +`rung_53_scenario_period_volume_limit.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 53: a `transmission_volume_expansion_limit` row holds in every scenario under `multi_investment_periods`. + +PyPSA 1.3.0 builds no such row on a network with scenarios and investment +periods (PyPSA/PyPSA#1939). The two futures are identical, so the oracle is the +same network without scenarios, which PyPSA solves with the row. +""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +ISSUE = 1939 +OPTIMIZE = {'multi_investment_periods': True} + + +def network(): + """Two periods, a cheap unit behind an extendable line whose volume a limit of 60 caps.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', ['hill', 'town']) + n.add('Carrier', 'AC') + n.add('Generator', 'hydro53', bus='hill', p_nom=100, marginal_cost=5) + n.add('Generator', 'diesel53', bus='town', p_nom=100, marginal_cost=90) + n.add( + 'Line', + 'tie53', + bus0='hill', + bus1='town', + x=0.1, + carrier='AC', + length=3, + s_nom_extendable=True, + s_nom_max=100, + capital_cost=1, + build_year=2020, + lifetime=30, + ) + n.add('Load', 'town_load', bus='town', p_set=[40, 50, 60, 45]) + n.add( + 'GlobalConstraint', + 'volume53', + type='transmission_volume_expansion_limit', + carrier_attribute='AC', + sense='<=', + constant=60, + ) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 13de643e..9db4910d 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1899,5 +1899,73 @@ "Generator-fix-p-lower": 4, "Generator-fix-p-upper": 4 } + }, + "rung_52_scenario_cost_limit": { + "columns": { + "Generator-p": 16, + "Link-p": 16, + "Link-p_nom": 1 + }, + "diverges": { + "issue": 1939, + "objective": 11890.0 + }, + "global_constraints": { + "('calm', 'cost52')": { + "sense": "<=", + "type": "transmission_expansion_cost_limit" + }, + "('stormy', 'cost52')": { + "sense": "<=", + "type": "transmission_expansion_cost_limit" + } + }, + "objective": 15630.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 16, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Link-ext-p-lower": 8, + "Link-ext-p-upper": 8, + "Link-ext-p_nom-lower": 2, + "Link-ext-p_nom-upper": 2, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8 + } + }, + "rung_53_scenario_period_volume_limit": { + "columns": { + "Generator-p": 16, + "Line-s": 8, + "Line-s_nom": 1 + }, + "diverges": { + "issue": 1939, + "objective": 1465.0 + }, + "global_constraints": { + "('calm', 'volume53')": { + "sense": "<=", + "type": "transmission_volume_expansion_limit" + }, + "('stormy', 'volume53')": { + "sense": "<=", + "type": "transmission_volume_expansion_limit" + } + }, + "objective": 15005.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 16, + "Generator-fix-p-lower": 16, + "Generator-fix-p-upper": 16, + "Line-ext-s-lower": 8, + "Line-ext-s-upper": 8, + "Line-ext-s_nom-lower": 2, + "Line-ext-s_nom-upper": 2 + } } } diff --git a/examples/references/pypsa/rung_52_scenario_cost_limit.py b/examples/references/pypsa/rung_52_scenario_cost_limit.py new file mode 100644 index 00000000..74820c40 --- /dev/null +++ b/examples/references/pypsa/rung_52_scenario_cost_limit.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 52: a `transmission_expansion_cost_limit` row holds in every scenario. + +PyPSA 1.3.0 builds no such row on a network with scenarios (PyPSA/PyPSA#1939). +The two futures are identical, so the oracle is the same network without +scenarios, which PyPSA solves with the row. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1939 + + +def network(): + """The spine plus an extendable DC link whose build a cost limit of 150 caps.""" + n = spine.build() + n.add('Carrier', 'DC') + n.add( + 'Link', + 'hvdc52', + bus0='north', + bus1='south', + carrier='DC', + p_nom_extendable=True, + p_nom_max=100, + capital_cost=10, + ) + n.add('Load', 'port52', bus='south', p_set=40) + n.add( + 'GlobalConstraint', + 'cost52', + type='transmission_expansion_cost_limit', + carrier_attribute='DC', + sense='<=', + constant=150, + ) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] diff --git a/examples/references/pypsa/rung_53_scenario_period_volume_limit.py b/examples/references/pypsa/rung_53_scenario_period_volume_limit.py new file mode 100644 index 00000000..afe570d0 --- /dev/null +++ b/examples/references/pypsa/rung_53_scenario_period_volume_limit.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 53: a `transmission_volume_expansion_limit` row holds in every scenario under `multi_investment_periods`. + +PyPSA 1.3.0 builds no such row on a network with scenarios and investment +periods (PyPSA/PyPSA#1939). The two futures are identical, so the oracle is the +same network without scenarios, which PyPSA solves with the row. +""" + +from __future__ import annotations + +from datetime import datetime + +import pandas as pd + +ISSUE = 1939 +OPTIMIZE = {'multi_investment_periods': True} + + +def network(): + """Two periods, a cheap unit behind an extendable line whose volume a limit of 60 caps.""" + import pypsa + + n = pypsa.Network() + n.snapshots = pd.MultiIndex.from_tuples( + [(2020, datetime(2020, 1, 1, t)) for t in range(2)] + [(2030, datetime(2030, 1, 1, t)) for t in range(2)] + ) + n.investment_periods = [2020, 2030] + n.investment_period_weightings['objective'] = [1.0, 0.5] + n.investment_period_weightings['years'] = [10.0, 10.0] + n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0] + n.add('Bus', ['hill', 'town']) + n.add('Carrier', 'AC') + n.add('Generator', 'hydro53', bus='hill', p_nom=100, marginal_cost=5) + n.add('Generator', 'diesel53', bus='town', p_nom=100, marginal_cost=90) + n.add( + 'Line', + 'tie53', + bus0='hill', + bus1='town', + x=0.1, + carrier='AC', + length=3, + s_nom_extendable=True, + s_nom_max=100, + capital_cost=1, + build_year=2020, + lifetime=30, + ) + n.add('Load', 'town_load', bus='town', p_set=[40, 50, 60, 45]) + n.add( + 'GlobalConstraint', + 'volume53', + type='transmission_volume_expansion_limit', + carrier_attribute='AC', + sense='<=', + constant=60, + ) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] From f3e42112dc965d5d61aa5d49da5b27e3e2231298 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 13:35:35 +0200 Subject: [PATCH 44/46] 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/examples/pypsa.md | 117 ++++++++++++++++-- examples/pypsa.yaml | 22 ++-- examples/references/pypsa/references.json | 24 ++++ .../pypsa/rung_54_scenario_delay.py | 65 ++++++++++ 4 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 examples/references/pypsa/rung_54_scenario_delay.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index f50aaffb..58208f8e 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -3479,7 +3479,7 @@ calm efficiency in both, to `16992.0`; with both calm values in both, to | PyPSA | status | note | | --- | --- | --- | | [objective](#objective), [`Bus-nodal_balance`](#bus-nodal_balance) with a cost and an efficiency per scenario | done | `Generator_marginal_cost` and `Link_efficiency` over `scenario` | -| a link `delay` or `cyclic_delay` that differs by scenario | out | PyPSA `1.3.0` groups the ports by delay over all scenarios and shifts each group in every scenario, so a delay of `0` in one future and `1` in the other solves below both uniform networks (`constraints.py:1269`). The file holds one delay for every scenario | +| a link `delay` or `cyclic_delay` that differs by scenario | diverges | rung 54, [PyPSA/PyPSA#1941](https://github.com/PyPSA/PyPSA/issues/1941) | | a transformer in a cycle on a network with scenarios | out | PyPSA `1.3.0` fails: it selects the transformers of a cycle by name from a table indexed by scenario and name (`constraints.py:1654`). The file holds one phase shift for every scenario | | a committable component on a network with scenarios | out | PyPSA `1.3.0` fails: it selects the status by snapshot and name where the first dimension is the scenario (`constraints.py:1872`, `:1942`). The file builds the rows per scenario | | [`{c}-p_nom_set`](#generator-p_nom_set) on a network with scenarios | out | PyPSA `1.3.0` fails: it reindexes the build by a table indexed by scenario and name (`constraints.py:1708`). The file builds the row per scenario | @@ -4318,6 +4318,109 @@ def oracle(): +### Rung 54 — a delay per scenario + +`n.set_scenarios(...)` with a link `delay` and a process `delay1` that differ +by scenario. The file states `Link_output_delay`, `Link_output_cyclic_delay`, +`Process_output_delay` and `Process_output_cyclic_delay` over `scenario`, so +each future shifts a port's flow by its own delay. The shifted flow already +spans `scenario`, so the offset may too. PyPSA `1.3.0` groups the ports by +delay over all scenarios and shifts each group in every scenario, so a port +whose delay differs by scenario delivers its flow once per group +(`constraints.py:1269-1276`, +[PyPSA/PyPSA#1941](https://github.com/PyPSA/PyPSA/issues/1941)). A plain run +feeds one scenario, and the rows collapse to the standard ones. + +The rung is a capped source feeding two sinks, one through a link and one +through a process. The calm future delivers at once. The stormy one delivers a +snapshot late, cyclically on the link and with the first snapshot lost on the +process. Nothing is extendable, so the futures do not interact. The oracle is +each future solved alone, `7650.0` calm and `11500.0` stormy, weighted `0.6` +and `0.4`: `9190.0`. PyPSA solves to `9300.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Link_output_arrival`](#link_output_arrival), [`Process_output_arrival`](#process_output_arrival) with a `delay` or `cyclic_delay` that differs by scenario | diverges | [PyPSA/PyPSA#1941](https://github.com/PyPSA/PyPSA/issues/1941); the delays span `scenario` | + + +> ✘ `pypsa 1.3.0` solves this rung's network at objective `9300.0`, 104 rows, [PyPSA/PyPSA#1941](https://github.com/PyPSA/PyPSA/issues/1941). The intended objective is `9190.0`. + +
+The network, as PyPSA code + +`rung_54_scenario_delay.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 54: each scenario delays a link's and a process's flow by its own `delay`. + +PyPSA 1.3.0 groups the ports by delay over all scenarios and shifts every group +in every scenario, so a port whose delay differs by scenario delivers twice +(PyPSA/PyPSA#1941). Nothing is extendable, so the scenarios do not interact: the +oracle is each future solved alone, weighted by its probability. +""" + +from __future__ import annotations + +from datetime import datetime + +ISSUE = 1941 + +#: The `generators` weighting is uniform, as on rung 16, so a delay of `n` is a +#: shift of exactly `n` positions. The `objective` column stays non-uniform. +WEIGHTINGS = {'objective': [2.0, 1.5, 2.5, 3.0], 'generators': [1.0] * 4} +DEMAND = [20.0, 35.0, 5.0, 30.0] +SCENARIOS = {'calm': 0.6, 'stormy': 0.4} + +#: each future's own delay, per port: the calm one delivers at once, the stormy one a snapshot late +DELAYS = { + 'calm': {'pipe54': {'delay': 0, 'cyclic_delay': True}, 'conv54': {'delay1': 0, 'cyclic_delay1': True}}, + 'stormy': {'pipe54': {'delay': 1, 'cyclic_delay': True}, 'conv54': {'delay1': 1, 'cyclic_delay1': False}}, +} + + +def network(delays: dict[str, dict[str, object]]): + """A capped source feeding two sinks, one through a link and one through a process, with the given delays.""" + import pypsa + + n = pypsa.Network() + n.set_snapshots([datetime(2015, 1, 1, hour) for hour in range(4)]) + for column, values in WEIGHTINGS.items(): + n.snapshot_weightings[column] = values + n.add('Bus', ['source', 'sink_link', 'sink_process']) + n.add('Generator', 'spring54', bus='source', p_nom=50, marginal_cost=5) + n.add('Generator', 'backup_link54', bus='sink_link', p_nom=200, marginal_cost=100) + n.add('Generator', 'backup_process54', bus='sink_process', p_nom=200, marginal_cost=100) + n.add('Link', 'pipe54', bus0='source', bus1='sink_link', p_nom=30, **delays['pipe54']) + n.add('Process', 'conv54', bus0='source', bus1='sink_process', p_nom=30, **delays['conv54']) + n.add('Load', 'load_link54', bus='sink_link', p_set=DEMAND) + n.add('Load', 'load_process54', bus='sink_process', p_set=DEMAND) + return n + + +def build(): + """The network over two futures, each with its own delays.""" + n = network(DELAYS['calm']) + n.set_scenarios(SCENARIOS) + for scenario, ports in DELAYS.items(): + for name, values in ports.items(): + component = n.c.links if name == 'pipe54' else n.c.processes + for column, value in values.items(): + component.static.loc[(scenario, name), column] = value + return n + + +def oracle(): + """Each future alone, with its own delays, weighted by its probability.""" + return [(weight, network(DELAYS[scenario])) for scenario, weight in SCENARIOS.items()] +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -4418,8 +4521,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | | $`\overline{\mathrm{f}}`$ | `Link_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most flow, per unit of nominal power | | $`\eta`$ | `Link_efficiency` over $`\Xi \times \mathcal{O}`$ — share of the flow that arrives at an output port, PyPSA's `efficiency`, `efficiency2`, … read long — negative where that port consumes rather than delivers | -| $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once. One for every scenario: PyPSA groups the ports by delay over all scenarios and shifts each group in every one (`constraints.py:1269`) | -| $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at each period's first snapshots is lost. One for every scenario, as the delay | +| $`\mathrm{d}^{f}`$ | `Link_output_delay` over $`\Xi \times \mathcal{O}`$ — snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once. Each scenario takes its own. PyPSA `1.3.0` groups the ports by delay over all scenarios and shifts each group in every one, so a delay that differs by scenario delivers the flow twice (`constraints.py:1269-1276`, PyPSA/PyPSA\#1941) | +| $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\Xi \times \mathcal{O}`$ — whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at each period's first snapshots is lost. Each scenario takes its own, as the delay | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | | $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | | $`\mathrm{com}^{f}`$ | `Link_committable` over $`\mathcal{L}`$ — whether flow is gated by an on/off status decision | @@ -4448,8 +4551,8 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\underline{\mathrm{z}}`$ | `Process_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — least internal power, per unit of nominal power — negative for a process that runs both ways | | $`\overline{\mathrm{z}}`$ | `Process_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most internal power, per unit of nominal power | | $`\alpha`$ | `Process_rate` over $`\Xi \times \mathcal{R}`$ — the energy a port draws or delivers per unit of internal power, PyPSA's `rate0`, `rate1`, … read long — negative where the port withdraws, positive where it injects; a link is a process whose `bus0` rate is minus one and whose output rates are its efficiencies | -| $`\mathrm{d}^{z}`$ | `Process_output_delay` over $`\mathcal{R}`$ — snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that transfers at once. One for every scenario, as a link's | -| $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost. One for every scenario, as the delay | +| $`\mathrm{d}^{z}`$ | `Process_output_delay` over $`\Xi \times \mathcal{R}`$ — snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that transfers at once. Each scenario takes its own, as a link's | +| $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\Xi \times \mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost. Each scenario takes its own, as the delay | | $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | | $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | | $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | @@ -9447,7 +9550,7 @@ Link_output_arrival: ``` ```math -\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} +\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,o} & \text{if } \mathrm{cyc}^{f}_{\xi,o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} ``` ### `Process_output_arrival` @@ -9470,7 +9573,7 @@ Process_output_arrival: ``` ```math -\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} +\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,r} & \text{if } \mathrm{cyc}^{z}_{\xi,r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} ``` ### `GlobalConstraint_energy_weight` diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 55a73a03..ee3127d4 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -236,17 +236,19 @@ parameters: snapshots a port's delivery lags its link's flow — PyPSA's `delay`, `delay2`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that delivers at once. - One for every scenario: PyPSA groups the ports by delay over all scenarios - and shifts each group in every one (`constraints.py:1269`) - dims: [link_output] + Each scenario takes its own. PyPSA `1.3.0` groups the ports by delay over + all scenarios and shifts each group in every one, so a delay that differs + by scenario delivers the flow twice (`constraints.py:1269-1276`, + PyPSA/PyPSA#1941) + dims: [scenario, link_output] dtype: int Link_output_cyclic_delay: description: >- whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, - the flow still in transit at each period's first snapshots is lost. One - for every scenario, as the delay - dims: [link_output] + the flow still in transit at each period's first snapshots is lost. Each + scenario takes its own, as the delay + dims: [scenario, link_output] dtype: bool Link_marginal_cost: description: cost of one unit of flow @@ -378,16 +380,16 @@ parameters: snapshots a port's transfer lags its process's internal power — PyPSA's `delay0`, `delay1`, … read long, in `snapshot_weightings.generators` units, which the file states as whole snapshots; zero for a port that - transfers at once. One for every scenario, as a link's - dims: [process_output] + transfers at once. Each scenario takes its own, as a link's + dims: [scenario, process_output] dtype: int Process_output_cyclic_delay: description: >- whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost. - One for every scenario, as the delay - dims: [process_output] + Each scenario takes its own, as the delay + dims: [scenario, process_output] dtype: bool Process_marginal_cost: description: cost of one unit of internal power diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index 9db4910d..a1ee7d04 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1967,5 +1967,29 @@ "Line-ext-s_nom-lower": 2, "Line-ext-s_nom-upper": 2 } + }, + "rung_54_scenario_delay": { + "columns": { + "Generator-p": 24, + "Link-p": 8, + "Process-p": 8 + }, + "diverges": { + "issue": 1941, + "objective": 9300.0 + }, + "global_constraints": {}, + "objective": 9190.0, + "objective_constant": 0.0, + "pypsa": "1.3.0", + "rows": { + "Bus-nodal_balance": 24, + "Generator-fix-p-lower": 24, + "Generator-fix-p-upper": 24, + "Link-fix-p-lower": 8, + "Link-fix-p-upper": 8, + "Process-fix-p-lower": 8, + "Process-fix-p-upper": 8 + } } } diff --git a/examples/references/pypsa/rung_54_scenario_delay.py b/examples/references/pypsa/rung_54_scenario_delay.py new file mode 100644 index 00000000..ac2bbafa --- /dev/null +++ b/examples/references/pypsa/rung_54_scenario_delay.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 54: each scenario delays a link's and a process's flow by its own `delay`. + +PyPSA 1.3.0 groups the ports by delay over all scenarios and shifts every group +in every scenario, so a port whose delay differs by scenario delivers twice +(PyPSA/PyPSA#1941). Nothing is extendable, so the scenarios do not interact: the +oracle is each future solved alone, weighted by its probability. +""" + +from __future__ import annotations + +from datetime import datetime + +ISSUE = 1941 + +#: The `generators` weighting is uniform, as on rung 16, so a delay of `n` is a +#: shift of exactly `n` positions. The `objective` column stays non-uniform. +WEIGHTINGS = {'objective': [2.0, 1.5, 2.5, 3.0], 'generators': [1.0] * 4} +DEMAND = [20.0, 35.0, 5.0, 30.0] +SCENARIOS = {'calm': 0.6, 'stormy': 0.4} + +#: each future's own delay, per port: the calm one delivers at once, the stormy one a snapshot late +DELAYS = { + 'calm': {'pipe54': {'delay': 0, 'cyclic_delay': True}, 'conv54': {'delay1': 0, 'cyclic_delay1': True}}, + 'stormy': {'pipe54': {'delay': 1, 'cyclic_delay': True}, 'conv54': {'delay1': 1, 'cyclic_delay1': False}}, +} + + +def network(delays: dict[str, dict[str, object]]): + """A capped source feeding two sinks, one through a link and one through a process, with the given delays.""" + import pypsa + + n = pypsa.Network() + n.set_snapshots([datetime(2015, 1, 1, hour) for hour in range(4)]) + for column, values in WEIGHTINGS.items(): + n.snapshot_weightings[column] = values + n.add('Bus', ['source', 'sink_link', 'sink_process']) + n.add('Generator', 'spring54', bus='source', p_nom=50, marginal_cost=5) + n.add('Generator', 'backup_link54', bus='sink_link', p_nom=200, marginal_cost=100) + n.add('Generator', 'backup_process54', bus='sink_process', p_nom=200, marginal_cost=100) + n.add('Link', 'pipe54', bus0='source', bus1='sink_link', p_nom=30, **delays['pipe54']) + n.add('Process', 'conv54', bus0='source', bus1='sink_process', p_nom=30, **delays['conv54']) + n.add('Load', 'load_link54', bus='sink_link', p_set=DEMAND) + n.add('Load', 'load_process54', bus='sink_process', p_set=DEMAND) + return n + + +def build(): + """The network over two futures, each with its own delays.""" + n = network(DELAYS['calm']) + n.set_scenarios(SCENARIOS) + for scenario, ports in DELAYS.items(): + for name, values in ports.items(): + component = n.c.links if name == 'pipe54' else n.c.processes + for column, value in values.items(): + component.static.loc[(scenario, name), column] = value + return n + + +def oracle(): + """Each future alone, with its own delays, weighted by its probability.""" + return [(weight, network(DELAYS[scenario])) for scenario, weight in SCENARIOS.items()] From 720275cd39f5ac430e6d343283f7b2c02ec62dee Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 13:35:53 +0200 Subject: [PATCH 45/46] 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/examples/pypsa.md | 310 +++++++++++++++++- examples/references/pypsa/references.json | 32 ++ .../rung_55_scenario_transformer_cycle.py | 40 +++ .../rung_56_scenario_security_constrained.py | 41 +++ .../pypsa/rung_57_scenario_nom_set.py | 43 +++ .../pypsa/rung_58_scenario_committable.py | 47 +++ 6 files changed, 508 insertions(+), 5 deletions(-) create mode 100644 examples/references/pypsa/rung_55_scenario_transformer_cycle.py create mode 100644 examples/references/pypsa/rung_56_scenario_security_constrained.py create mode 100644 examples/references/pypsa/rung_57_scenario_nom_set.py create mode 100644 examples/references/pypsa/rung_58_scenario_committable.py diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 58208f8e..edc74ac1 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -2545,7 +2545,7 @@ bind, in `Transformer-fix-s-lower` against a line outage and in | [`Line-fix-s-*-security-for-{c}-outage-in-sub-network-{n}`](#line-fix-s-lower-security-for-c-outage-in-sub-network-n), [`Line-ext-s-*-security-…`](#line-ext-s-lower-security-for-c-outage-in-sub-network-n) | split | PyPSA names a row per outaged component and sub-network; one block over the `outage` axis | | [`Transformer-fix-s-*-security-…`](#transformer-fix-s-lower-security-for-c-outage-in-sub-network-n), [`Transformer-ext-s-*-security-…`](#transformer-ext-s-lower-security-for-c-outage-in-sub-network-n) | split | the same for a transformer | | a branch not active in a period | done | PyPSA keeps the copy with that branch's flow dropped, so the file reads its flow as zero there; a copy left with no variable is not built here, where linopy counts it; no rung records it | -| a security-constrained run over scenarios | out | PyPSA `1.3.0` raises, see [Refusals](#refusals) | +| a security-constrained run over scenarios | diverges | rung 56, [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942) | | `transmission_losses`, `linearized_unit_commitment` in a security-constrained run | done | PyPSA builds neither, so the copies carry no loss term and data prep feeds `transmission_losses` false; no rung, since rung 30 is lossless | @@ -3480,9 +3480,9 @@ calm efficiency in both, to `16992.0`; with both calm values in both, to | --- | --- | --- | | [objective](#objective), [`Bus-nodal_balance`](#bus-nodal_balance) with a cost and an efficiency per scenario | done | `Generator_marginal_cost` and `Link_efficiency` over `scenario` | | a link `delay` or `cyclic_delay` that differs by scenario | diverges | rung 54, [PyPSA/PyPSA#1941](https://github.com/PyPSA/PyPSA/issues/1941) | -| a transformer in a cycle on a network with scenarios | out | PyPSA `1.3.0` fails: it selects the transformers of a cycle by name from a table indexed by scenario and name (`constraints.py:1654`). The file holds one phase shift for every scenario | -| a committable component on a network with scenarios | out | PyPSA `1.3.0` fails: it selects the status by snapshot and name where the first dimension is the scenario (`constraints.py:1872`, `:1942`). The file builds the rows per scenario | -| [`{c}-p_nom_set`](#generator-p_nom_set) on a network with scenarios | out | PyPSA `1.3.0` fails: it reindexes the build by a table indexed by scenario and name (`constraints.py:1708`). The file builds the row per scenario | +| a transformer in a cycle on a network with scenarios | diverges | rung 55, [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942) | +| a committable component on a network with scenarios | diverges | rung 58, [PyPSA/PyPSA#1913](https://github.com/PyPSA/PyPSA/issues/1913) | +| [`{c}-p_nom_set`](#generator-p_nom_set) on a network with scenarios | diverges | rung 57, [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942) | > ✔ `pypsa 1.3.0` solves this rung's network at objective `17964.0`, 96 rows. @@ -4421,6 +4421,306 @@ def oracle(): +### Rung 55 — a transformer cycle per scenario + +`n.set_scenarios(...)` with two transformers in parallel, a cycle. The file +builds the Kirchhoff voltage row in every scenario. PyPSA `1.3.0` raises +`KeyError`: it selects the transformers of a cycle by name from a table indexed +by scenario and name (`constraints.py:1654`, +[PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942)). + +The rung adds two transformers of reactance `0.1` and `0.2`, each rated `30`, +to the spine. The row splits the flow two to one, so the first caps the pair at +`45`. The two futures are identical. The oracle is the same network without +scenarios, which PyPSA solves to `13105.0`. One transformer rated `60` solves to +`11705.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Kirchhoff-Voltage-Law`](#kirchhoff-voltage-law) with a transformer on a network with scenarios | diverges | [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942); the row per scenario. The file holds one phase shift for every scenario | + + +> ✘ `pypsa 1.3.0` raises `KeyError` on this rung's network, [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942). The intended objective is `13105.0`. + +
+The network, as PyPSA code + +`rung_55_scenario_transformer_cycle.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 55: a cycle of two transformers takes its Kirchhoff voltage row in every scenario. + +PyPSA 1.3.0 raises on a transformer in a cycle on a network with scenarios +(PyPSA/PyPSA#1942). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1942 + + +def network(): + """The spine plus two parallel transformers of unequal reactance, so the one that takes more flow caps the pair.""" + n = spine.build() + n.add('Bus', ['a', 'b']) + n.add('Generator', 'hydro55', bus='a', p_nom=100, marginal_cost=10) + n.add('Generator', 'diesel55', bus='b', p_nom=100, marginal_cost=50) + n.add('Load', 'town55', bus='b', p_set=[50, 40, 55, 45]) + n.add('Transformer', 't55', bus0='a', bus1='b', x=0.1, s_nom=30) + n.add('Transformer', 't55_2', bus0='a', bus1='b', x=0.2, s_nom=30) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] +``` + +
+ + +### Rung 56 — a security-constrained run per scenario + +`n.optimize.optimize_security_constrained(...)` on a network with scenarios. +The file builds the outage copies in every scenario. PyPSA `1.3.0` raises +`ValueError`. With outages named as a list, it finds none of them in the +network (`abstract.py:427`). With no outages named, it fails to intersect the +branches (`abstract.py:445`, +[PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942)). With outages +named as `(component, name)` pairs, it builds no copy and solves without them. + +The rung adds two parallel lines rated `30` and `35` to the spine, and outages +each. Each line must carry the whole import alone, so the import falls to `30`. +The two futures are identical. The oracle is the same network without +scenarios, which PyPSA solves to `18205.0`. A plain `n.optimize()` solves to +`11705.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`Line-fix-s-*-security-for-{c}-outage-in-sub-network-{n}`](#line-fix-s-lower-security-for-c-outage-in-sub-network-n) on a network with scenarios | diverges | [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942); the copies per scenario | + + +> ✘ `pypsa 1.3.0` raises `ValueError` on this rung's network, [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942). The intended objective is `18205.0`. + +
+The network, as PyPSA code + +`rung_56_scenario_security_constrained.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 56: a security-constrained run over scenarios copies its rows into every scenario. + +PyPSA 1.3.0 raises on a security-constrained run on a network with scenarios +(PyPSA/PyPSA#1942). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1942 +BRANCH_OUTAGES = ['l56', 'l56_2'] + + +def network(): + """The spine plus two parallel lines, so each must carry the whole import alone when the other is out.""" + n = spine.build() + n.add('Bus', ['a', 'b']) + n.add('Generator', 'hydro56', bus='a', p_nom=100, marginal_cost=10) + n.add('Generator', 'diesel56', bus='b', p_nom=100, marginal_cost=50) + n.add('Load', 'town56', bus='b', p_set=[50, 40, 55, 45]) + n.add('Line', 'l56', bus0='a', bus1='b', x=0.1, s_nom=30) + n.add('Line', 'l56_2', bus0='a', bus1='b', x=0.1, s_nom=35) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] +``` + +
+ + +### Rung 57 — a fixed build per scenario + +`n.set_scenarios(...)` with `p_nom_set` on an extendable unit. The file builds +`Generator-p_nom_set` in every scenario. PyPSA `1.3.0` raises `TypeError`: it +renames the scenario-and-name index of the set build with one name +(`constraints.py:1708`, +[PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942)). The same holds +for every `*_nom_set`. + +The rung adds a cheap extendable wind unit to the spine, pinned to `20`. The +two futures are identical. The oracle is the same network without scenarios, +which PyPSA solves to `4800.0`. Without `p_nom_set`, the unit builds `67` and +the network solves to `335.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| [`{c}-p_nom_set`](#generator-p_nom_set) on a network with scenarios | diverges | [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942); the row per scenario | + + +> ✘ `pypsa 1.3.0` raises `TypeError` on this rung's network, [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942). The intended objective is `4800.0`. + +
+The network, as PyPSA code + +`rung_57_scenario_nom_set.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 57: `p_nom_set` pins an extendable build on a network with scenarios. + +PyPSA 1.3.0 raises on any `*_nom_set` on a network with scenarios +(PyPSA/PyPSA#1942). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1942 + + +def network(): + """The spine plus a cheap extendable wind unit whose build is pinned below what it would choose.""" + n = spine.build() + n.add( + 'Generator', + 'wind57', + bus='south', + p_nom_extendable=True, + p_nom_max=100, + capital_cost=5, + p_nom_set=20, + ) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] +``` + +
+ + +### Rung 58 — a committable unit per scenario + +`n.set_scenarios(...)` with a committable unit. The file builds the status +rows in every scenario. PyPSA `1.3.0` raises `KeyError`: it selects the status +by snapshot and name where the first dimension is the scenario +(`constraints.py:1872`, +[PyPSA/PyPSA#1913](https://github.com/PyPSA/PyPSA/issues/1913)). + +The rung adds a cheap committable unit to the spine that cannot run below `40` +% of its build, with a start-up cost of `100`. The two futures are identical. +The oracle is the same network without scenarios, which PyPSA solves to +`7430.0`. The same unit, not committable, solves to `7330.0`. + +| PyPSA | status | note | +| --- | --- | --- | +| a committable component on a network with scenarios | diverges | [PyPSA/PyPSA#1913](https://github.com/PyPSA/PyPSA/issues/1913); the rows per scenario | + + +> ✘ `pypsa 1.3.0` raises `KeyError` on this rung's network, [PyPSA/PyPSA#1913](https://github.com/PyPSA/PyPSA/issues/1913). The intended objective is `7430.0`. + +
+The network, as PyPSA code + +`rung_58_scenario_committable.py` + +```python +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 58: a committable unit takes its status rows in every scenario. + +PyPSA 1.3.0 raises on a committable component on a network with scenarios +(PyPSA/PyPSA#1913). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1913 + + +def network(): + """The spine plus a cheap committable unit that cannot run below 40 % of its build.""" + n = spine.build() + n.add( + 'Generator', + 'uc58', + bus='north', + committable=True, + p_nom=50, + marginal_cost=5, + p_min_pu=0.4, + min_up_time=2, + up_time_before=0, + start_up_cost=100, + ) + n.add('Load', 'swing58', bus='north', p_set=[5, 45, 45, 10]) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] +``` + +
+ + ## Refusals Where PyPSA refuses to build, parity means refusing too. None is a language @@ -4440,7 +4740,7 @@ where it should live — language, data prep, or harness — is one open questio | `UnboundLocalError`, `global_constraints.py:375`, `:602` | a `primary_energy` or `operational_limit` row that names an `investment_period` without `multi_investment_periods` | data prep, at `GlobalConstraint_counts_snapshot` | | | `ValueError`, `constraints.py:2411`, `:2518` | an extendable lossy branch with `s_nom_max = inf`, either mode | data prep, at `Line_loss_max` and `Transformer_loss_max` | X4 | | `RuntimeError`, `constraints.py:2561` | the secant loop passing `max_segments` | data prep, at the `segment` axis | X4 | -| `ValueError`, `abstract.py:427`, `:445` | a security-constrained run over scenarios | rows per scenario, not refused | | +| `ValueError`, `abstract.py:427`, `:445` | a security-constrained run over scenarios | rows per scenario, not refused: a PyPSA bug, [PyPSA/PyPSA#1942](https://github.com/PyPSA/PyPSA/issues/1942), rung 56 | | | `NotImplementedError`, `global_constraints.py:66-68` | a `tech_capacity_expansion_limit` row on a network with scenarios | assumed where there is more than one scenario: [`GlobalConstraint_tech_capacity_expansion_limit_without_scenarios`](#globalconstraint_tech_capacity_expansion_limit_without_scenarios). The file cannot tell one scenario from none, which PyPSA also refuses | | | `ConsistencyError`, `consistency.py:1506-1560` | a maintainable component whose `maintenance_duration` or `maintenance_events` is not positive, whose events do not fit the weighted horizon, or that is extendable with `p_nom_max = inf` | assumed: [`Generator_maintenance_events_positive`](#generator_maintenance_events_positive), [`-duration_positive`](#generator_maintenance_duration_positive), [`-duration_fits_the_horizon`](#generator_maintenance_duration_fits_the_horizon), [`-events_fit_the_horizon`](#generator_maintenance_events_fit_the_horizon), [`-build_cap_is_finite`](#generator_maintenance_build_cap_is_finite), and the `Link` and `Process` ones | | | nothing; HiGHS refuses the model, `constraints.py:500-503` | a fixed modular committable maintainable build, whose module count `p_nom_max / p_nom_mod` is infinite | assumed: [`Generator_maintenance_module_count_is_finite`](#generator_maintenance_module_count_is_finite), and the `Link` and `Process` ones | | diff --git a/examples/references/pypsa/references.json b/examples/references/pypsa/references.json index a1ee7d04..e94d92fe 100644 --- a/examples/references/pypsa/references.json +++ b/examples/references/pypsa/references.json @@ -1991,5 +1991,37 @@ "Process-fix-p-lower": 8, "Process-fix-p-upper": 8 } + }, + "rung_55_scenario_transformer_cycle": { + "diverges": { + "issue": 1942, + "raises": "KeyError" + }, + "objective": 13105.0, + "pypsa": "1.3.0" + }, + "rung_56_scenario_security_constrained": { + "diverges": { + "issue": 1942, + "raises": "ValueError" + }, + "objective": 18205.0, + "pypsa": "1.3.0" + }, + "rung_57_scenario_nom_set": { + "diverges": { + "issue": 1942, + "raises": "TypeError" + }, + "objective": 4800.0, + "pypsa": "1.3.0" + }, + "rung_58_scenario_committable": { + "diverges": { + "issue": 1913, + "raises": "KeyError" + }, + "objective": 7430.0, + "pypsa": "1.3.0" } } diff --git a/examples/references/pypsa/rung_55_scenario_transformer_cycle.py b/examples/references/pypsa/rung_55_scenario_transformer_cycle.py new file mode 100644 index 00000000..d2b274b0 --- /dev/null +++ b/examples/references/pypsa/rung_55_scenario_transformer_cycle.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 55: a cycle of two transformers takes its Kirchhoff voltage row in every scenario. + +PyPSA 1.3.0 raises on a transformer in a cycle on a network with scenarios +(PyPSA/PyPSA#1942). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1942 + + +def network(): + """The spine plus two parallel transformers of unequal reactance, so the one that takes more flow caps the pair.""" + n = spine.build() + n.add('Bus', ['a', 'b']) + n.add('Generator', 'hydro55', bus='a', p_nom=100, marginal_cost=10) + n.add('Generator', 'diesel55', bus='b', p_nom=100, marginal_cost=50) + n.add('Load', 'town55', bus='b', p_set=[50, 40, 55, 45]) + n.add('Transformer', 't55', bus0='a', bus1='b', x=0.1, s_nom=30) + n.add('Transformer', 't55_2', bus0='a', bus1='b', x=0.2, s_nom=30) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] diff --git a/examples/references/pypsa/rung_56_scenario_security_constrained.py b/examples/references/pypsa/rung_56_scenario_security_constrained.py new file mode 100644 index 00000000..9bc487e4 --- /dev/null +++ b/examples/references/pypsa/rung_56_scenario_security_constrained.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 56: a security-constrained run over scenarios copies its rows into every scenario. + +PyPSA 1.3.0 raises on a security-constrained run on a network with scenarios +(PyPSA/PyPSA#1942). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1942 +BRANCH_OUTAGES = ['l56', 'l56_2'] + + +def network(): + """The spine plus two parallel lines, so each must carry the whole import alone when the other is out.""" + n = spine.build() + n.add('Bus', ['a', 'b']) + n.add('Generator', 'hydro56', bus='a', p_nom=100, marginal_cost=10) + n.add('Generator', 'diesel56', bus='b', p_nom=100, marginal_cost=50) + n.add('Load', 'town56', bus='b', p_set=[50, 40, 55, 45]) + n.add('Line', 'l56', bus0='a', bus1='b', x=0.1, s_nom=30) + n.add('Line', 'l56_2', bus0='a', bus1='b', x=0.1, s_nom=35) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] diff --git a/examples/references/pypsa/rung_57_scenario_nom_set.py b/examples/references/pypsa/rung_57_scenario_nom_set.py new file mode 100644 index 00000000..f221c420 --- /dev/null +++ b/examples/references/pypsa/rung_57_scenario_nom_set.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 57: `p_nom_set` pins an extendable build on a network with scenarios. + +PyPSA 1.3.0 raises on any `*_nom_set` on a network with scenarios +(PyPSA/PyPSA#1942). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1942 + + +def network(): + """The spine plus a cheap extendable wind unit whose build is pinned below what it would choose.""" + n = spine.build() + n.add( + 'Generator', + 'wind57', + bus='south', + p_nom_extendable=True, + p_nom_max=100, + capital_cost=5, + p_nom_set=20, + ) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] diff --git a/examples/references/pypsa/rung_58_scenario_committable.py b/examples/references/pypsa/rung_58_scenario_committable.py new file mode 100644 index 00000000..61a2645a --- /dev/null +++ b/examples/references/pypsa/rung_58_scenario_committable.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""Rung 58: a committable unit takes its status rows in every scenario. + +PyPSA 1.3.0 raises on a committable component on a network with scenarios +(PyPSA/PyPSA#1913). The two futures are identical, so the oracle is the same +network without scenarios. +""" + +from __future__ import annotations + +import spine + +ISSUE = 1913 + + +def network(): + """The spine plus a cheap committable unit that cannot run below 40 % of its build.""" + n = spine.build() + n.add( + 'Generator', + 'uc58', + bus='north', + committable=True, + p_nom=50, + marginal_cost=5, + p_min_pu=0.4, + min_up_time=2, + up_time_before=0, + start_up_cost=100, + ) + n.add('Load', 'swing58', bus='north', p_set=[5, 45, 45, 10]) + return n + + +def build(): + """The same network over two identical futures.""" + n = network() + n.set_scenarios({'calm': 0.6, 'stormy': 0.4}) + return n + + +def oracle(): + """The network without scenarios: the futures are identical, so the expected cost is its cost.""" + return [(1.0, network())] From 60c279e559ceaef3a5a8ebdf10e098b7d0a79909 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 25 Sep 2026 13:36:01 +0200 Subject: [PATCH 46/46] docs(pypsa): the first-snapshot ramp refusals link the open PyPSA question --- docs/examples/pypsa.md | 9 ++++++--- examples/pypsa.yaml | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index edc74ac1..648e3fd6 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -11068,7 +11068,8 @@ Generator_came_in_running_unless_committable: start-up ramp where another unit of the component is committable with a fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents the attribute as read only for a committable unit and does not check - it. The spec does not state that row, so it refuses the data + it. PyPSA has not decided which row is intended (PyPSA/PyPSA#1943). The + spec does not state that row, so it refuses the data ``` ```math @@ -11088,7 +11089,8 @@ Link_came_in_running_unless_committable: start-up ramp where another link of the component is committable with a fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents the attribute as read only for a committable link and does not check - it. The spec does not state that row, so it refuses the data + it. PyPSA has not decided which row is intended (PyPSA/PyPSA#1943). The + spec does not state that row, so it refuses the data ``` ```math @@ -11108,7 +11110,8 @@ Process_came_in_running_unless_committable: start-up ramp where another process of the component is committable with a fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents the attribute as read only for a committable process and does not check - it. The spec does not state that row, so it refuses the data + it. PyPSA has not decided which row is intended (PyPSA/PyPSA#1943). The + spec does not state that row, so it refuses the data ``` ```math diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index ee3127d4..4c898373 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -3989,7 +3989,8 @@ assumptions: start-up ramp where another unit of the component is committable with a fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents the attribute as read only for a committable unit and does not check - it. The spec does not state that row, so it refuses the data + it. PyPSA has not decided which row is intended (PyPSA/PyPSA#1943). The + spec does not state that row, so it refuses the data Link_came_in_running_unless_committable: holds: "Link_status_initial == 1" where: "NOT Link_committable AND (Link_ramp_limit_up OR Link_ramp_limit_down)" @@ -4000,7 +4001,8 @@ assumptions: start-up ramp where another link of the component is committable with a fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents the attribute as read only for a committable link and does not check - it. The spec does not state that row, so it refuses the data + it. PyPSA has not decided which row is intended (PyPSA/PyPSA#1943). The + spec does not state that row, so it refuses the data Process_came_in_running_unless_committable: holds: "Process_status_initial == 1" where: "NOT Process_committable AND (Process_ramp_limit_up OR Process_ramp_limit_down)" @@ -4011,7 +4013,8 @@ assumptions: start-up ramp where another process of the component is committable with a fixed build (`constraints.py:1091-1094`, `1110-1112`). PyPSA documents the attribute as read only for a committable process and does not check - it. The spec does not state that row, so it refuses the data + it. PyPSA has not decided which row is intended (PyPSA/PyPSA#1943). The + spec does not state that row, so it refuses the data objective: sense: minimize