PyPSA, the relaxed commitment#
Rung 12 of PyPSA in one file: n.optimize(linearized_unit_commitment=True), stated on rungs 1 and 7 in a
file of its own — the model's description below says why. Its network is the spine plus the script's own additions.
Rung 12 — linearized unit commitment#
| PyPSA | status | note |
|---|---|---|
Generator-status, -start_up, -shut_down |
done | shares in [0, 1], not binaries |
Generator-com-p-before |
done | where start and stop cost the same — a data-prep bool |
Generator-com-p-current |
done | |
Generator-com-partly-start-up |
done | |
Generator-com-partly-shut-down |
done |
✔
pypsa 1.3.0solves this rung's network at objective7775.0, 128 rows.
The network, as PyPSA code
rung_12_linearized_uc.py
# SPDX-FileCopyrightText: math-spec 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 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.
Sets#
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) — snapshot — dispatch periods |
| \(\mathcal{N}\) | index \(n\) — bus — 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}\) — 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},\enspace \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 |
\(\mathrm{pos}(t)\) denotes where index \(t\) sits along its dimension's own order — the order shift walks, 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#
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)
Generator-fix-p-lower#
Generator_fix_p_lower
Generator_fix_p_lower:
description: "`Generator-fix-p-lower` — a generator outputs at least its minimum"
foreach: [snapshot, generator]
where: not Generator_committable
expression: Generator_p >= Generator_p_min_pu * Generator_p_nom
Generator-fix-p-upper#
Generator_fix_p_upper
Generator_fix_p_upper:
description: "`Generator-fix-p-upper` — a generator outputs at most what is available"
foreach: [snapshot, generator]
where: not Generator_committable
expression: Generator_p <= Generator_p_max_pu * Generator_p_nom
Link-fix-p-lower#
Link_fix_p_lower
Link_fix_p_lower:
description: "`Link-fix-p-lower` — a link carries at least its minimum, negative for the other way"
foreach: [snapshot, link]
expression: Link_p >= Link_p_min_pu * Link_p_nom
Link-fix-p-upper#
Link_fix_p_upper
Link_fix_p_upper:
description: "`Link-fix-p-upper` — a link carries at most its nominal power"
foreach: [snapshot, link]
expression: Link_p <= Link_p_max_pu * Link_p_nom
Bus-nodal_balance#
Bus_nodal_balance
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
foreach: [snapshot, bus]
expression: >-
sum(Generator_p, by=Generator_bus)
- sum(Link_p, by=Link_bus0)
+ sum(at(Link_p, by=Link_output_link) * Link_efficiency, by=Link_output_bus)
== sum(Load_p_set, by=Load_bus)
Generator-com-p-lower#
Generator_com_p_lower
Generator_com_p_lower:
description: "`Generator-com-p-lower` — a committed unit outputs at least its minimum; off, at least nothing"
foreach: [snapshot, generator]
where: Generator_committable
expression: Generator_p >= Generator_p_min_pu * Generator_p_nom * Generator_status
Generator-com-p-upper#
Generator_com_p_upper
Generator_com_p_upper:
description: "`Generator-com-p-upper` — a committed unit outputs at most what is available; off, at most nothing"
foreach: [snapshot, generator]
where: Generator_committable
expression: Generator_p <= Generator_p_max_pu * Generator_p_nom * Generator_status
Generator-com-transition-start-up#
Generator_com_transition_start_up
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"
foreach: [snapshot, generator]
where: Generator_committable
expression: Generator_start_up >= Generator_status - Generator_previous_status
Generator-com-transition-shut-down#
Generator_com_transition_shut_down
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"
foreach: [snapshot, generator]
where: Generator_committable
expression: Generator_shut_down >= Generator_previous_status - Generator_status
Generator-com-up-time#
Generator_com_up_time
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
foreach: [snapshot, generator]
where: Generator_committable AND Generator_min_up_time > 0 AND position(snapshot) > 0
expression: sum_back(Generator_start_up, over=snapshot, within=Generator_min_up_time) <= Generator_status
Generator-com-down-time#
Generator_com_down_time
Generator_com_down_time:
description: "`Generator-com-down-time` — a unit stopped within its own minimum down time is still off"
foreach: [snapshot, generator]
where: Generator_committable AND Generator_min_down_time > 0 AND position(snapshot) > 0
expression: sum_back(Generator_shut_down, over=snapshot, within=Generator_min_down_time) <= 1 - Generator_status
Generator-com-status-min_up_time_must_stay_up#
Generator_com_status_must_stay_up
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"
foreach: [snapshot, generator]
where: Generator_committable AND Generator_must_stay_up
expression: Generator_status == 1
Generator-p-ramp_limit_up#
Generator_p_ramp_limit_up
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
foreach: [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#
Generator_p_ramp_limit_down
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
foreach: [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#
Generator_status_p_fixed_upper
Generator_status_p_fixed_upper:
description: "`Generator-status-p-fixed-upper` — a status is at most one, an explicit row as PyPSA writes it"
foreach: [snapshot, generator]
where: Generator_committable
expression: Generator_status <= 1
Generator-start_up-p-fixed-upper#
Generator_start_up_p_fixed_upper
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"
foreach: [snapshot, generator]
where: Generator_committable
expression: Generator_start_up <= 1
Generator-shut_down-p-fixed-upper#
Generator_shut_down_p_fixed_upper
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"
foreach: [snapshot, generator]
where: Generator_committable
expression: Generator_shut_down <= 1
Generator-com-p-before#
Generator_com_p_before
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
foreach: [snapshot, generator]
where: Generator_committable AND Generator_partly_tightened
expression: >-
shift(Generator_p, over=snapshot, offset=1)
- Generator_ramp_limit_shut_down * Generator_p_nom * shift(Generator_status, over=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#
Generator_com_p_current
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"
foreach: [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#
Generator_com_partly_start_up
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"
foreach: [snapshot, generator]
where: Generator_committable AND Generator_partly_tightened
expression: >-
Generator_p - shift(Generator_p, over=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, over=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#
Generator_com_partly_shut_down
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"
foreach: [snapshot, generator]
where: Generator_committable AND Generator_partly_tightened
expression: >-
shift(Generator_p, over=snapshot, offset=1) - Generator_p
- Generator_ramp_limit_shut_down * Generator_p_nom * shift(Generator_status, over=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
Variable domains#
Generator_p
Link_p
Generator_status
Generator_start_up
Generator_shut_down
Regenerate with pixi run python -m tools.gallery.