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¶
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 7400.0 on both sides; structure ✔ 21 constraints · 8 variables, name for name; size ✔ 191 rows · ✔ 100 columns · ✔ 378 nonzeros; duals — integer model, no duals; model for model: 27 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 |
3 | 3 |
Generator-com-p-lower |
16 | 16 |
Generator-com-p-upper |
16 | 16 |
Generator-com-status-min_down_time_must_stay_up |
2 | 2 |
Generator-com-transition-shut-down |
16 | 16 |
Generator-com-transition-start-up |
16 | 16 |
Generator-fix-p-lower |
12 | 12 |
Generator-fix-p-upper |
12 | 12 |
Generator-maint-event-count |
2 | 2 |
Generator-maint-status-lb |
4 | 4 |
Generator-maint-status-le-maint |
4 | 4 |
Generator-maint-status-le-status |
4 | 4 |
Generator-maint-window |
8 | 8 |
Generator-p-ramp_limit_down |
4 | 4 |
Generator-p-ramp_limit_up |
8 | 8 |
Generator-shut_down-p-fixed-upper |
16 | 16 |
Generator-start_up-p-fixed-upper |
16 | 16 |
Generator-status-p-fixed-upper |
16 | 16 |
Link-fix-p-lower |
4 | 4 |
Link-fix-p-upper |
4 | 4 |
| column | PyPSA | specsolve |
|---|---|---|
Generator-maintenance |
8 | 8 |
Generator-maintenance_start |
8 | 8 |
Generator-maintenance_status |
4 | 4 |
Generator-p |
28 | 28 |
Generator-shut_down |
16 | 16 |
Generator-start_up |
16 | 16 |
Generator-status |
16 | 16 |
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. 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¶
| Symbol | Meaning |
|---|---|
| \(\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},\ \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 |
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{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{T} \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{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{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{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{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{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 |
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 |
| \(\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¶
| 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 — 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 |
| \(\mathit{Generator\_ramp\_up\_allowance}\) | 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 |
| \(\mathit{Generator\_ramp\_down\_allowance}\) | 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{Generator\_ramp\_up\_rate}\) | 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 |
| \(\mathrm{Generator\_ramp\_down\_rate}\) | 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 |
| \(\mathrm{Generator\_start\_up\_rate}\) | 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 |
| \(\mathrm{Generator\_shut\_down\_rate}\) | 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.
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_down_time
Generator_com_status_must_stay_down
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_maint_event_count
Generator_maint_window
Generator_maint_status_le_status
Generator_maint_status_le_maint
Generator_maint_status_lb
Definitions¶
Generator_previous_status
Generator_previous_p
Generator_ramp_up_allowance
Generator_ramp_down_allowance
Generator_ramp_up_rate
Generator_ramp_down_rate
Generator_start_up_rate
Generator_shut_down_rate
Variable domains¶
Generator_p
Link_p
Generator_status
Generator_start_up
Generator_shut_down
Generator_maintenance
Generator_maintenance_start
Generator_maintenance_status
The spec, differential/pypsa/rungs/rung_44_linearized_commitment.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. 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: {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}
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}
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_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]
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: [snapshot, link_output]
Link_marginal_cost:
description: cost of one unit of flow
dims: [snapshot, link]
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 — 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 — 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]
Generator_ramp_limit_shut_down:
description: most output in the snapshot before a unit stops, per unit of nominal power
dims: [generator]
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_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_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]
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_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
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}
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}
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 * (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 * (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]
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. Each generator and load term enters with
its component''s `sign` (`constraints.py:1428-1429`, `:1538`)'
dims: [snapshot, bus]
expression: 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_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]
where: Generator_committable
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 - 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]
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_down_time:
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
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 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 carries a row at the first snapshot only where its `p_init`
gives the output it brought in'
dims: [snapshot, generator]
where: (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 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 carries a row at the first snapshot only where its `p_init`
gives the output it brought in'
dims: [snapshot, generator]
where: (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]
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_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_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
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 — 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: Generator_status_initial * Generator_p_init}
otherwise: shift(Generator_p, along=snapshot, offset=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
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: [snapshot, 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: [snapshot, 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
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
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),
**{
dim: pl.Series(dim, list(names(n.static(component).index).astype(str)), dtype=pl.String)
for component, dim in DIM.items()
},
**scenarios(n),
**periods(n),
**carriers(n, multi),
'Generator_bus': relation(n, 'Generator', 'bus'),
'Link_bus0': relation(n, 'Link', 'bus0'),
'Load_bus': relation(n, 'Load', 'bus'),
'snapshot_weightings_objective': weighting(n, 'objective'),
'Generator_sign': per_component('Generator', first_scenario(n.generators['sign'])),
'Load_p_set': varying(n, 'Load', 'p_set'),
'Load_sign': per_component('Load', first_scenario(loads['sign'])),
}
with sps.solve('differential/pypsa/rungs/rung_44_linearized_commitment.yaml', sources) as solution:
solution.objective # 7400.0
The network, rung_44_linearized_commitment.py in the corpus — the spine plus what this rung adds:
# SPDX-FileCopyrightText: mathspec 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 data¶
The tables this rung is the first to declare (5), as the prep produced them:
Generator_maintainable.csv
generator,value
coal,false
down44,false
fixed44,true
gas,false
maint44,true
pulse44,false
ramp44,false
Generator_maintenance_cover.csv
generator,start,covered
fixed44,2015-01-01T00:00:00.000000,2015-01-01T00:00:00.000000
fixed44,2015-01-01T01:00:00.000000,2015-01-01T01:00:00.000000
fixed44,2015-01-01T01:00:00.000000,2015-01-01T02:00:00.000000
fixed44,2015-01-01T02:00:00.000000,2015-01-01T02:00:00.000000
fixed44,2015-01-01T03:00:00.000000,2015-01-01T03:00:00.000000
maint44,2015-01-01T00:00:00.000000,2015-01-01T00:00:00.000000
maint44,2015-01-01T00:00:00.000000,2015-01-01T01:00:00.000000
maint44,2015-01-01T01:00:00.000000,2015-01-01T01:00:00.000000
maint44,2015-01-01T01:00:00.000000,2015-01-01T02:00:00.000000
maint44,2015-01-01T02:00:00.000000,2015-01-01T02:00:00.000000
maint44,2015-01-01T03:00:00.000000,2015-01-01T03:00:00.000000
Generator_maintenance_events.csv
Generator_maintenance_pu.csv
Generator_must_stay_down.csv