Rung 12: linearized unit commitment — the status a share in [0, 1], stated by pypsa_linearized_uc.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 7775.0 on both sides; structure ✔ 21 constraints · 5 variables, name for name; size ✔ 128 rows · ✔ 44 columns · ✔ 288 nonzeros; duals ≠ 128 rows, 1 negated,
Generator-com-status-min_up_time_must_stay_upoff by 490.0 — degenerate with PyPSA's duplicated cap — a unit held on has status pinned at 1 by this row from below and by the variable bound and the cap row from above, and HiGHS may put the dual on any of them;Generator-status-p-fixed-upperoff by 1447.5 — PyPSA bounds the status at 1 on the variable and writes the cap row as well, so a binding cap's dual may sit on the bound and leave the row at zero; the file states the row alone (rung 12, the linearized relaxation); model for model: 24 blocks equal, 0 documented splits, 3 recorded deviations.
Rows and columns, PyPSA against specsolve, name for name
| row | PyPSA | specsolve |
|---|---|---|
Bus-nodal_balance |
8 | 8 |
Generator-com-down-time |
6 | 6 |
Generator-com-p-before |
3 | 3 |
Generator-com-p-current |
3 | 3 |
Generator-com-p-lower |
8 | 8 |
Generator-com-p-upper |
8 | 8 |
Generator-com-partly-shut-down |
3 | 3 |
Generator-com-partly-start-up |
3 | 3 |
Generator-com-status-min_up_time_must_stay_up |
2 | 2 |
Generator-com-transition-shut-down |
8 | 8 |
Generator-com-transition-start-up |
8 | 8 |
Generator-com-up-time |
6 | 6 |
Generator-fix-p-lower |
8 | 8 |
Generator-fix-p-upper |
8 | 8 |
Generator-p-ramp_limit_down |
7 | 7 |
Generator-p-ramp_limit_up |
7 | 7 |
Generator-shut_down-p-fixed-upper |
8 | 8 |
Generator-start_up-p-fixed-upper |
8 | 8 |
Generator-status-p-fixed-upper |
8 | 8 |
Link-fix-p-lower |
4 | 4 |
Link-fix-p-upper |
4 | 4 |
| column | PyPSA | specsolve |
|---|---|---|
Generator-p |
16 | 16 |
Generator-shut_down |
8 | 8 |
Generator-start_up |
8 | 8 |
Generator-status |
8 | 8 |
Link-p |
4 | 4 |
The model¶
The same model, as math
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 spec'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.
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{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{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{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 |
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 |
| \(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 |
Definitions¶
| Symbol | Meaning |
|---|---|
| \(\mathit{Generator\_previous\_status}\) | 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 |
| \(\mathit{Generator\_previous\_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 |
\(\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.
Objective¶
Subject to¶
Generator_fix_p_lower
Generator_fix_p_upper
Link_fix_p_lower
Link_fix_p_upper
Bus_nodal_balance
Generator_com_p_lower
Generator_com_p_upper
Generator_com_transition_start_up
Generator_com_transition_shut_down
Generator_com_up_time
Generator_com_down_time
Generator_com_status_must_stay_up
Generator_p_ramp_limit_up
Generator_p_ramp_limit_down
Generator_status_p_fixed_upper
Generator_start_up_p_fixed_upper
Generator_shut_down_p_fixed_upper
Generator_com_p_before
Generator_com_p_current
Generator_com_partly_start_up
Generator_com_partly_shut_down
Definitions¶
Generator_previous_status
Generator_previous_p
Variable domains¶
Generator_p
Link_p
Generator_status
Generator_start_up
Generator_shut_down
The spec, differential/pypsa/rungs/rung_12_linearized_uc.yaml — the file projected onto what this rung builds:
description: '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 spec''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.'
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'}
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:
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]
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]
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]
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]
Generator_ramp_limit_start_up:
description: most output in the snapshot a unit starts, per unit of nominal power
dims: [generator]
Generator_ramp_limit_shut_down:
description: most output in the snapshot before a unit stops, per unit of nominal power
dims: [generator]
Generator_min_up_time:
description: least snapshots a unit stays on once started
dims: [generator]
dtype: int
Generator_min_down_time:
description: least snapshots a unit stays off once stopped
dims: [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]
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]
dtype: bool
Generator_start_up_cost:
description: cost of one start
dims: [generator]
Generator_shut_down_cost:
description: cost of one stop
dims: [generator]
Generator_stand_by_cost:
description: cost of one snapshot spent on
dims: [snapshot, generator]
Generator_partly_tightened:
description: 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
dims: [generator]
dtype: bool
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]
Generator_status:
description: '`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'
dims: [snapshot, generator]
where: Generator_committable
bounds: {lower: 0}
Generator_start_up:
description: '`Generator-start_up` — how much of a committable unit turns on this snapshot'
dims: [snapshot, generator]
where: Generator_committable
bounds: {lower: 0}
Generator_shut_down:
description: '`Generator-shut_down` — how much of a committable unit turns off this snapshot'
dims: [snapshot, generator]
where: Generator_committable
bounds: {lower: 0}
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
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
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)
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
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
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
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
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
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
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
expression: Generator_status == 1
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 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'
dims: [snapshot, generator]
where: Generator_committable AND Generator_ramp_limit_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_status - Generator_previous_status)
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 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'
dims: [snapshot, generator]
where: Generator_committable AND Generator_ramp_limit_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_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'
dims: [snapshot, generator]
where: Generator_committable
expression: Generator_status <= 1
Generator_start_up_p_fixed_upper:
description: '`Generator-start_up-p-fixed-upper` — a start is at most one, an explicit row as PyPSA
writes it'
dims: [snapshot, generator]
where: Generator_committable
expression: Generator_start_up <= 1
Generator_shut_down_p_fixed_upper:
description: '`Generator-shut_down-p-fixed-upper` — a stop is at most one, an explicit row as PyPSA
writes it'
dims: [snapshot, generator]
where: Generator_committable
expression: Generator_shut_down <= 1
Generator_com_p_before:
description: '`Generator-com-p-before` — the output a unit had entering this snapshot fits the share
of it still on, less the share it is shutting down at the shut-down ramp. The translated term vacates
the first snapshot, as PyPSA''s `sns[1:]` does'
dims: [snapshot, generator]
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_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'
dims: [snapshot, generator]
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_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
* 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_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'
dims: [snapshot, generator]
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_start_up <= 0
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]
cases:
opening: {when: position(snapshot) == 0, expression: Generator_status_initial}
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
dims: [snapshot, generator]
cases:
opening: {when: position(snapshot) == 0, expression: 0}
otherwise: shift(Generator_p, along=snapshot, offset=1)
objective: {sense: minimize, description: 'operating cost by weighted snapshot, plus what starts, stops
and standing by cost', expression: sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective)
+ sum(Link_p * Link_marginal_cost * snapshot_weightings_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)}
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 _must_stay_up(n: pypsa.Network) -> pd.DataFrame:
"""True while the up time a unit brought into the horizon still binds."""
rows = []
for name, g in n.generators.iterrows():
if not g['committable'] or g['up_time_before'] <= 0:
continue
remaining = int(min(g['min_up_time'] - g['up_time_before'], len(n.snapshots)))
rows.extend({'snapshot': t, 'generator': str(name), 'value': True} for t in timesteps(n)[: max(remaining, 0)])
table = pd.DataFrame(rows, columns=['snapshot', 'generator', 'value'])
return table.astype({'value': bool})
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_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'),
'Generator_committable': static(n, 'Generator', 'committable'),
'Generator_ramp_limit_up': static(n, 'Generator', 'ramp_limit_up').dropna(),
'Generator_ramp_limit_down': static(n, 'Generator', 'ramp_limit_down').dropna(),
'Generator_ramp_limit_start_up': static(n, 'Generator', 'ramp_limit_start_up').fillna({'value': 1.0}),
'Generator_ramp_limit_shut_down': static(n, 'Generator', 'ramp_limit_shut_down').fillna({'value': 1.0}),
'Generator_min_up_time': static(n, 'Generator', 'min_up_time'),
'Generator_min_down_time': static(n, 'Generator', 'min_down_time'),
'Generator_status_initial': pd.DataFrame(
keyed(generators.index, 'generator')
| {
'value': (generators['up_time_before'] > 0).astype(int).to_numpy(),
}
),
'Generator_must_stay_up': _must_stay_up(n),
'Generator_start_up_cost': static(n, 'Generator', 'start_up_cost'),
'Generator_shut_down_cost': static(n, 'Generator', 'shut_down_cost'),
'Generator_stand_by_cost': varying(n, 'Generator', 'stand_by_cost'),
'Generator_partly_tightened': pd.DataFrame(
keyed(generators.index, 'generator')
| {
'value': (generators['start_up_cost'] == generators['shut_down_cost']).to_numpy(),
}
),
**loss_fan(n, segments),
}
with sps.solve('differential/pypsa/rungs/rung_12_linearized_uc.yaml', sources) as solution:
solution.objective # 7775.0
The network, rung_12_linearized_uc.py in the corpus — the spine plus what this rung adds:
# SPDX-FileCopyrightText: mathspec Contributors
#
# SPDX-License-Identifier: MIT
"""Rung 12: linearized unit commitment — the status a share in [0, 1], stated by `pypsa_linearized_uc.yaml`."""
from __future__ import annotations
import spine
MODEL = 'pypsa_linearized_uc.yaml'
OPTIMIZE = {'linearized_unit_commitment': True}
def build():
"""The spine plus two committable units, one whose start and stop cost the same, so PyPSA tightens its relaxation."""
n = spine.build()
n.add(
'Generator',
'uc12',
bus='north',
committable=True,
p_nom=50,
marginal_cost=5,
p_min_pu=0.4,
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=100,
stand_by_cost=5,
)
n.add(
'Generator',
'cold12',
bus='south',
committable=True,
p_nom=30,
marginal_cost=60,
p_min_pu=0.3,
min_up_time=2,
min_down_time=1,
up_time_before=0,
ramp_limit_up=0.5,
ramp_limit_down=0.5,
ramp_limit_start_up=0.7,
ramp_limit_shut_down=0.7,
start_up_cost=80,
shut_down_cost=40,
)
n.add('Load', 'swing12', bus='north', p_set=[25, 45, 45, 10])
return n
The data¶
The tables this rung is the first to declare (35), as the prep produced them:
Generator_bus.csv
Generator_committable.csv
Generator_marginal_cost.csv
snapshot,generator,value
2015-01-01T00:00:00.000000,coal,10.0
2015-01-01T00:00:00.000000,cold12,60.0
2015-01-01T00:00:00.000000,gas,30.0
2015-01-01T00:00:00.000000,uc12,5.0
2015-01-01T01:00:00.000000,coal,10.0
2015-01-01T01:00:00.000000,cold12,60.0
2015-01-01T01:00:00.000000,gas,30.0
2015-01-01T01:00:00.000000,uc12,5.0
2015-01-01T02:00:00.000000,coal,10.0
2015-01-01T02:00:00.000000,cold12,60.0
2015-01-01T02:00:00.000000,gas,30.0
2015-01-01T02:00:00.000000,uc12,5.0
2015-01-01T03:00:00.000000,coal,10.0
2015-01-01T03:00:00.000000,cold12,60.0
2015-01-01T03:00:00.000000,gas,30.0
2015-01-01T03:00:00.000000,uc12,5.0
Generator_min_down_time.csv
Generator_min_up_time.csv
Generator_must_stay_up.csv
Generator_p_max_pu.csv
snapshot,generator,value
2015-01-01T00:00:00.000000,coal,1.0
2015-01-01T00:00:00.000000,cold12,1.0
2015-01-01T00:00:00.000000,gas,1.0
2015-01-01T00:00:00.000000,uc12,1.0
2015-01-01T01:00:00.000000,coal,1.0
2015-01-01T01:00:00.000000,cold12,1.0
2015-01-01T01:00:00.000000,gas,1.0
2015-01-01T01:00:00.000000,uc12,1.0
2015-01-01T02:00:00.000000,coal,1.0
2015-01-01T02:00:00.000000,cold12,1.0
2015-01-01T02:00:00.000000,gas,1.0
2015-01-01T02:00:00.000000,uc12,1.0
2015-01-01T03:00:00.000000,coal,1.0
2015-01-01T03:00:00.000000,cold12,1.0
2015-01-01T03:00:00.000000,gas,1.0
2015-01-01T03:00:00.000000,uc12,1.0
Generator_p_min_pu.csv
snapshot,generator,value
2015-01-01T00:00:00.000000,coal,0.0
2015-01-01T00:00:00.000000,cold12,0.3
2015-01-01T00:00:00.000000,gas,0.0
2015-01-01T00:00:00.000000,uc12,0.4
2015-01-01T01:00:00.000000,coal,0.0
2015-01-01T01:00:00.000000,cold12,0.3
2015-01-01T01:00:00.000000,gas,0.0
2015-01-01T01:00:00.000000,uc12,0.4
2015-01-01T02:00:00.000000,coal,0.0
2015-01-01T02:00:00.000000,cold12,0.3
2015-01-01T02:00:00.000000,gas,0.0
2015-01-01T02:00:00.000000,uc12,0.4
2015-01-01T03:00:00.000000,coal,0.0
2015-01-01T03:00:00.000000,cold12,0.3
2015-01-01T03:00:00.000000,gas,0.0
2015-01-01T03:00:00.000000,uc12,0.4
Generator_p_nom.csv
Generator_partly_tightened.csv
Generator_ramp_limit_down.csv
Generator_ramp_limit_shut_down.csv
Generator_ramp_limit_start_up.csv
Generator_ramp_limit_up.csv
Generator_shut_down_cost.csv
Generator_stand_by_cost.csv
snapshot,generator,value
2015-01-01T00:00:00.000000,coal,0.0
2015-01-01T00:00:00.000000,cold12,0.0
2015-01-01T00:00:00.000000,gas,0.0
2015-01-01T00:00:00.000000,uc12,5.0
2015-01-01T01:00:00.000000,coal,0.0
2015-01-01T01:00:00.000000,cold12,0.0
2015-01-01T01:00:00.000000,gas,0.0
2015-01-01T01:00:00.000000,uc12,5.0
2015-01-01T02:00:00.000000,coal,0.0
2015-01-01T02:00:00.000000,cold12,0.0
2015-01-01T02:00:00.000000,gas,0.0
2015-01-01T02:00:00.000000,uc12,5.0
2015-01-01T03:00:00.000000,coal,0.0
2015-01-01T03:00:00.000000,cold12,0.0
2015-01-01T03:00:00.000000,gas,0.0
2015-01-01T03:00:00.000000,uc12,5.0
Generator_start_up_cost.csv
Generator_status_initial.csv
Link_bus0.csv
Link_efficiency.csv
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_link.csv
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
Load_bus.csv
Load_p_set.csv
snapshot,load,value
2015-01-01T00:00:00.000000,north_load,30.0
2015-01-01T00:00:00.000000,south_load,40.0
2015-01-01T00:00:00.000000,swing12,25.0
2015-01-01T01:00:00.000000,north_load,30.0
2015-01-01T01:00:00.000000,south_load,40.0
2015-01-01T01:00:00.000000,swing12,45.0
2015-01-01T02:00:00.000000,north_load,30.0
2015-01-01T02:00:00.000000,south_load,40.0
2015-01-01T02:00:00.000000,swing12,45.0
2015-01-01T03:00:00.000000,north_load,30.0
2015-01-01T03:00:00.000000,south_load,40.0
2015-01-01T03:00:00.000000,swing12,10.0
bus.csv
generator.csv
link.csv
link_output.csv
load.csv
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