Skip to content

Rung 14: two futures and a risk preference — capacity chosen once, dispatch per scenario, stated by pypsa_stochastic.yaml

One rung of the PyPSA corpus: the file pypsa.yaml projected onto what this network builds, attached to that network, and held to what PyPSA solves it to.

✔ Verified against pypsa 1.3.0 — objective 9267.386667 on both sides; structure ≠ Generator-ext-p_nom-lower 2 vs 1 — PyPSA writes the build's floor once per scenario, each row over the one capacity variable; the file states it once — capacity is chosen before the future is known, and a copy per future is the same row again.; Generator-ext-p_nom-upper 2 vs 1 — PyPSA writes the build's cap once per scenario, each row over the one capacity variable; the file states it once — capacity is chosen before the future is known, and a copy per future is the same row again.; size ≠ 87 vs 85 rows · ✔ 37 columns · ≠ 148 vs 146 nonzeros; duals ≠ 87 rows, Generator-ext-p_nom-upper off by 36.714666667 — two copies of one binding row are degenerate — the solver may put the cap's whole price on either copy, so PyPSA's per-scenario duals are shares of the file's one.; model for model: 17 blocks equal, 0 documented splits, 2 recorded deviations.

Rows and columns, PyPSA against specsolve, name for name
row PyPSA specsolve
Bus-nodal_balance 16 16
CVaR-def 1 1
CVaR-excess-calm 1 1
CVaR-excess-stormy 1 1
Generator-ext-p-lower 8 8
Generator-ext-p-upper 8 8
Generator-ext-p_nom-lower 2 ≠ 1
Generator-ext-p_nom-upper 2 ≠ 1
Generator-fix-p-lower 16 16
Generator-fix-p-upper 16 16
Link-fix-p-lower 8 8
Link-fix-p-upper 8 8
column PyPSA specsolve
CVaR 1 1
CVaR-a 2 2
CVaR-theta 1 1
Generator-p 24 24
Generator-p_nom 1 1
Link-p 8 8

The model

The same model, as math

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

\[ \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 \]

Subject to

Generator_fix_p_lower

\[ 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

\[ 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

\[ 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

\[ 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

\[ P_{g} \ge \underline{\mathrm{p}}^{\mathrm{nom}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \]

Generator_ext_p_nom_upper

\[ 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

\[ 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

\[ 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

\[ \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

\[ a_{s} - \mathit{scenario\_opex}_{s} + \theta \ge 0 \qquad \forall\, s \in \mathcal{S} \]

CVaR_def

\[ \theta + \mathrm{v} \cdot \left( \sum_{s \in \mathcal{S}} \pi_{s} \cdot a_{s} \right) \le CVaR \]

Definitions

scenario_opex

\[ \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

\[ p_{s,t,g} \in \mathbb{R} \qquad \forall\, s \in \mathcal{S},\ t \in \mathcal{T},\ g \in \mathcal{G} \]

Link_p

\[ 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

\[ P_{g} \in \mathbb{R} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \]

CVaR_a

\[ a_{s} \ge 0 \qquad \forall\, s \in \mathcal{S} \]

CVaR_theta

\[ \theta \in \mathbb{R} \]

CVaR

\[ CVaR \in \mathbb{R} \]

The spec, differential/pypsa/rungs/rung_14_stochastic.yaml — the file projected onto what this rung builds:

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'}
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}
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]
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: []
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
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)'}
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'}

The prep — every table the spec declares, from the network — and the solve:

from differential.pypsa.prep import relation, static, varying, weighting


def _link_ports(n: pypsa.Network) -> pd.DataFrame:
    """A link's output ports read long — one row per port a link declares, carrying the link, the bus it delivers to and its efficiency.

    PyPSA spells the ports across columns — ``bus1``/``efficiency``, ``bus2``/``efficiency2``, … — and a
    link declares a port by naming a bus in one, so a link of any port count is as many rows here and
    one term in the balance. The label is the link and the column the port came from.
    """
    links = n.static('Link')
    blank = pd.Series('', index=links.index, dtype=str)
    frames = []
    for port in ['1', *n.components.links.additional_ports]:
        suffix = '' if port == '1' else port
        buses = links.get(f'bus{port}', blank).astype(str)
        # `efficiency`, `delay` and `cyclic_delay` are PyPSA's unsuffixed attributes: port 1
        # spells them bare and every port after it takes the number
        efficiencies = links.get(f'efficiency{suffix}', pd.Series(1.0, index=links.index)).astype(float)
        delays = links.get(f'delay{suffix}', pd.Series(0, index=links.index)).fillna(0).astype(int)
        cyclic = links.get(f'cyclic_delay{suffix}', pd.Series(False, index=links.index)).fillna(False).astype(bool)
        frame = pd.DataFrame(
            keyed(links.index, 'link')
            | {
                'bus': buses.to_numpy(),
                'value': efficiencies.to_numpy(),
                'delay': delays.to_numpy(),
                'cyclic_delay': cyclic.to_numpy(),
                'port': int(port),
            }
        )
        frames.append(frame[buses.to_numpy() != ''])
    ports = pd.concat(frames, ignore_index=True).sort_values(['link', 'port'], kind='stable')
    ports['link_output'] = ports['link'] + '_bus' + ports['port'].astype(str)
    return ports.drop(columns='port').reset_index(drop=True)


def _per_port(n: pypsa.Network, column: str, as_name: str | None = None) -> pd.DataFrame:
    """One column of the long port table keyed by ``link_output`` — what a port names, or what it carries.

    *as_name* is what the file calls it: a relation keeps its target dimension's
    own name, and every parameter over the ports lands under ``value``.
    """
    ports = _link_ports(n)
    keys = [key for key in ('scenario', 'link_output') if key in ports.columns]
    return ports[[*keys, column]].rename(columns={column: as_name or column})


n = build()  # the network from the PyPSA tab

sources = {
    'snapshot': pl.Series('snapshot', list(timesteps(n)), dtype=pl.Datetime('us')),
    'bus': pl.Series('bus', list(names(n.buses.index).astype(str)), dtype=pl.String),
    'generator': pl.Series('generator', list(names(generators.index).astype(str)), dtype=pl.String),
    'link': pl.Series('link', list(names(links.index).astype(str)), dtype=pl.String),
    'link_output': pl.Series('link_output', list(pd.unique(_link_ports(n)['link_output'])), dtype=pl.String),
    'load': pl.Series('load', list(names(loads.index).astype(str)), dtype=pl.String),
    'Generator_bus': relation(n, 'Generator', 'bus'),
    'Link_bus0': relation(n, 'Link', 'bus0'),
    'Link_output_link': _per_port(n, 'link'),
    'Link_output_bus': _per_port(n, 'bus'),
    'Load_bus': relation(n, 'Load', 'bus'),
    'snapshot_weightings_objective': weighting(n, 'objective'),
    'Generator_p_nom': static(n, 'Generator', 'p_nom'),
    'Generator_p_nom_extendable': static(n, 'Generator', 'p_nom_extendable'),
    'Generator_p_nom_min': static(n, 'Generator', 'p_nom_min'),
    'Generator_p_nom_max': static(n, 'Generator', 'p_nom_max'),
    'Generator_capital_cost': static(n, 'Generator', 'capital_cost'),
    'Generator_p_min_pu': varying(n, 'Generator', 'p_min_pu'),
    'Generator_p_max_pu': varying(n, 'Generator', 'p_max_pu'),
    'Generator_marginal_cost': varying(n, 'Generator', 'marginal_cost'),
    'Link_p_nom': static(n, 'Link', 'p_nom'),
    'Link_p_min_pu': varying(n, 'Link', 'p_min_pu'),
    'Link_p_max_pu': varying(n, 'Link', 'p_max_pu'),
    'Link_efficiency': _per_port(n, 'value'),
    'Link_marginal_cost': varying(n, 'Link', 'marginal_cost'),
    'Load_p_set': varying(n, 'Load', 'p_set'),
}

with sps.solve('differential/pypsa/rungs/rung_14_stochastic.yaml', sources) as solution:
    solution.objective  # 9267.386667

The network, rung_14_stochastic.py in the corpus — the spine plus what this rung adds:

# SPDX-FileCopyrightText: mathspec 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
n = build()
n.optimize(solver_name='highs')
n.objective  # 9267.386667

The data

The tables this rung is the first to declare (30), as the prep produced them:

CVaR_inv_tail.csv

value
2.0

CVaR_omega.csv

value
0.3

Generator_bus.csv

generator,bus
coal,north
gas,south
wind14,south

Generator_capital_cost.csv

generator,value
coal,0.0
gas,0.0
wind14,20.0

Generator_marginal_cost.csv

snapshot,generator,value
2015-01-01T00:00:00.000000,coal,10.0
2015-01-01T00:00:00.000000,gas,30.0
2015-01-01T00:00:00.000000,wind14,1.0
2015-01-01T01:00:00.000000,coal,10.0
2015-01-01T01:00:00.000000,gas,30.0
2015-01-01T01:00:00.000000,wind14,1.0
2015-01-01T02:00:00.000000,coal,10.0
2015-01-01T02:00:00.000000,gas,30.0
2015-01-01T02:00:00.000000,wind14,1.0
2015-01-01T03:00:00.000000,coal,10.0
2015-01-01T03:00:00.000000,gas,30.0
2015-01-01T03:00:00.000000,wind14,1.0

Generator_p_max_pu.csv

scenario,snapshot,generator,value
calm,2015-01-01T00:00:00.000000,coal,1.0
calm,2015-01-01T00:00:00.000000,gas,1.0
calm,2015-01-01T00:00:00.000000,wind14,0.9
calm,2015-01-01T01:00:00.000000,coal,1.0
calm,2015-01-01T01:00:00.000000,gas,1.0
calm,2015-01-01T01:00:00.000000,wind14,0.7
calm,2015-01-01T02:00:00.000000,coal,1.0
calm,2015-01-01T02:00:00.000000,gas,1.0
calm,2015-01-01T02:00:00.000000,wind14,0.8
calm,2015-01-01T03:00:00.000000,coal,1.0
calm,2015-01-01T03:00:00.000000,gas,1.0
calm,2015-01-01T03:00:00.000000,wind14,0.6
stormy,2015-01-01T00:00:00.000000,coal,1.0
stormy,2015-01-01T00:00:00.000000,gas,1.0
stormy,2015-01-01T00:00:00.000000,wind14,0.3
stormy,2015-01-01T01:00:00.000000,coal,1.0
stormy,2015-01-01T01:00:00.000000,gas,1.0
stormy,2015-01-01T01:00:00.000000,wind14,0.2
stormy,2015-01-01T02:00:00.000000,coal,1.0
stormy,2015-01-01T02:00:00.000000,gas,1.0
stormy,2015-01-01T02:00:00.000000,wind14,0.4
stormy,2015-01-01T03:00:00.000000,coal,1.0
stormy,2015-01-01T03:00:00.000000,gas,1.0
stormy,2015-01-01T03:00:00.000000,wind14,0.1

Generator_p_min_pu.csv

snapshot,generator,value
2015-01-01T00:00:00.000000,coal,0.0
2015-01-01T00:00:00.000000,gas,0.0
2015-01-01T00:00:00.000000,wind14,0.0
2015-01-01T01:00:00.000000,coal,0.0
2015-01-01T01:00:00.000000,gas,0.0
2015-01-01T01:00:00.000000,wind14,0.0
2015-01-01T02:00:00.000000,coal,0.0
2015-01-01T02:00:00.000000,gas,0.0
2015-01-01T02:00:00.000000,wind14,0.0
2015-01-01T03:00:00.000000,coal,0.0
2015-01-01T03:00:00.000000,gas,0.0
2015-01-01T03:00:00.000000,wind14,0.0

Generator_p_nom.csv

generator,value
coal,100.0
gas,100.0
wind14,0.0

Generator_p_nom_extendable.csv

generator,value
coal,false
gas,false
wind14,true

Generator_p_nom_max.csv

generator,value
coal,inf
gas,inf
wind14,100.0

Generator_p_nom_min.csv

generator,value
coal,0.0
gas,0.0
wind14,0.0

Link_bus0.csv

link,bus
wire,north

Link_efficiency.csv

link_output,value
wire_bus1,0.9

Link_marginal_cost.csv

snapshot,link,value
2015-01-01T00:00:00.000000,wire,0.0
2015-01-01T01:00:00.000000,wire,0.0
2015-01-01T02:00:00.000000,wire,0.0
2015-01-01T03:00:00.000000,wire,0.0

Link_output_bus.csv

link_output,bus
wire_bus1,south

Link_output_link.csv

link_output,link
wire_bus1,wire

Link_p_max_pu.csv

snapshot,link,value
2015-01-01T00:00:00.000000,wire,1.0
2015-01-01T01:00:00.000000,wire,1.0
2015-01-01T02:00:00.000000,wire,1.0
2015-01-01T03:00:00.000000,wire,1.0

Link_p_min_pu.csv

snapshot,link,value
2015-01-01T00:00:00.000000,wire,-1.0
2015-01-01T01:00:00.000000,wire,-1.0
2015-01-01T02:00:00.000000,wire,-1.0
2015-01-01T03:00:00.000000,wire,-1.0

Link_p_nom.csv

link,value
wire,40.0

Load_bus.csv

load,bus
north_load,north
port14,south
south_load,south

Load_p_set.csv

scenario,snapshot,load,value
calm,2015-01-01T00:00:00.000000,north_load,30.0
calm,2015-01-01T00:00:00.000000,port14,10.0
calm,2015-01-01T00:00:00.000000,south_load,40.0
calm,2015-01-01T01:00:00.000000,north_load,30.0
calm,2015-01-01T01:00:00.000000,port14,20.0
calm,2015-01-01T01:00:00.000000,south_load,40.0
calm,2015-01-01T02:00:00.000000,north_load,30.0
calm,2015-01-01T02:00:00.000000,port14,15.0
calm,2015-01-01T02:00:00.000000,south_load,40.0
calm,2015-01-01T03:00:00.000000,north_load,30.0
calm,2015-01-01T03:00:00.000000,port14,10.0
calm,2015-01-01T03:00:00.000000,south_load,40.0
stormy,2015-01-01T00:00:00.000000,north_load,30.0
stormy,2015-01-01T00:00:00.000000,port14,40.0
stormy,2015-01-01T00:00:00.000000,south_load,40.0
stormy,2015-01-01T01:00:00.000000,north_load,30.0
stormy,2015-01-01T01:00:00.000000,port14,60.0
stormy,2015-01-01T01:00:00.000000,south_load,40.0
stormy,2015-01-01T02:00:00.000000,north_load,30.0
stormy,2015-01-01T02:00:00.000000,port14,50.0
stormy,2015-01-01T02:00:00.000000,south_load,40.0
stormy,2015-01-01T03:00:00.000000,north_load,30.0
stormy,2015-01-01T03:00:00.000000,port14,30.0
stormy,2015-01-01T03:00:00.000000,south_load,40.0

bus.csv

bus
north
south

generator.csv

generator
coal
gas
wind14

link.csv

link
wire

link_output.csv

link_output
wire_bus1

load.csv

load
north_load
port14
south_load

scenario.csv

scenario
calm
stormy

scenario_weight.csv

scenario,value
calm,0.6
stormy,0.4

snapshot.csv

snapshot
2015-01-01T00:00:00.000000
2015-01-01T01:00:00.000000
2015-01-01T02:00:00.000000
2015-01-01T03:00:00.000000

snapshot_weightings_objective.csv

snapshot,value
2015-01-01T00:00:00.000000,2.0
2015-01-01T01:00:00.000000,1.5
2015-01-01T02:00:00.000000,2.5
2015-01-01T03:00:00.000000,3.0