Linearised unit commitment in PyPSA#
This page states the spec of the model that
n.optimize(linearized_unit_commitment=True) builds in PyPSA. The file
examples/variants/pypsa_linearized_uc.yaml states only the change to a base
spec (a patch), and its base is
PyPSA in one file.
override lays the patch over the
base:
import mathspec as ms
spec = ms.override('examples/pypsa.yaml', ['examples/variants/pypsa_linearized_uc.yaml'])
spec is the base with the patch laid over it.
The patch relaxes the status (variables.py:91-92), the start (:142-143)
and the stop (:193-194) of each committable unit. PyPSA refuses a modular
committable unit under the keyword (optimize.py:841-842). The other rows of
the integer run have their scenarios, periods and active conditions.
Five steps of the PyPSA ladder (rungs),
numbered 12, 44, 47, 67 and 74, run under the keyword. Four of them start from
a shared two-bus network (the spine), in
spine.py,
with the additions of the rung script. Rung 74 builds a network of its own.
Rung 12 — linearised unit commitment#
| PyPSA | status | note |
|---|---|---|
Generator-status, -start_up, -shut_down |
done | shares in [0, 1] |
Generator-com-p-before |
done | where start and stop cost the same in every snapshot and scenario, by a count over snapshot, then over scenario |
Generator-com-p-current |
done | |
Generator-com-partly-start-up |
done | |
Generator-com-partly-shut-down |
done |
✔
pypsa 1.4.0.post1.dev62+g3625c8168solves this rung's network at objective7775.0, 131 rows.
The network, as PyPSA code
rung_12_linearized_uc.py
# SPDX-FileCopyrightText: mathspec Contributors
#
# SPDX-License-Identifier: MIT
"""Rung 12: linearized unit commitment — the status a share in [0, 1], stated by the patch `variants/pypsa_linearized_uc.yaml`."""
from __future__ import annotations
import spine
PATCH = 'variants/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
Rung 44 — relaxed integer commitment rows#
Under the keyword, PyPSA builds the commitment rows of the integer run and adds
the four tightening rows (constraints.py:684-792). The keyword relaxes only the
status, start and stop to shares in [0, 1] (variables.py:91-92). The patch
leaves each of these rows to PyPSA in one file, and the rung's
network contains them under the keyword:
- A unit still serving the down time it brought in stays off, through its
down-time row (
constraints.py:663-683). - PyPSA builds a ramp row where a unit has a ramp limit, or only a start-up or
shut-down ramp (
constraints.py:1116-1125). A missing limit is the full build. - A maintainable unit is off for its events. The maintenance start stays a
binary under the keyword (
variables.py:239-264). The product of status and maintenance enters the commitment rows through themaint-statusrows (constraints.py:510-543). A maintainable unit that is not committable loses the same share of its fixed rows (constraints.py:153-163).
The rung adds five cheap units under a load that swings. A start costs more than a stop, so PyPSA builds no tightening rows for them, and each row is tight at the optimum. PyPSA solves the rung and its variants to these objectives (#977):
| network | objective |
|---|---|
| the rung as built | 7400.0 |
| without the down time brought in | 6540.0 |
| without the start-up ramp | 7164.0 |
the missing start-up and shut-down ramps as 0 |
8060.0 |
| the committable unit not maintainable | 7310.0 |
| the fixed unit not maintainable | 7100.0 |
| PyPSA | status | note |
|---|---|---|
Generator-com-down-time with the down time brought in |
done | |
Generator-p-ramp_limit_up, -down for a start-up or shut-down ramp alone, and a missing limit |
done | Generator_ramp_up_rate and the other three rates are 1 where the limit is missing |
Generator-maint-*, Generator-maint-status-* |
done | fixed units |
Generator-com-p-lower, -upper, Generator-fix-p-lower, -upper in maintenance |
done |
✔
pypsa 1.4.0.post1.dev62+g3625c8168solves this rung's network at objective7400.0, 190 rows.
The network, as PyPSA code
rung_44_linearized_commitment.py
# 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
PATCH = 'variants/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
Rung 47 — ramps and signs under the keyword#
Under the keyword, the rung's network contains five kinds of row, each on fixed builds. The patch states the first kind, the four tightening rows. PyPSA in one file states the other four:
- In the four tightening rows, a missing ramp limit is the full build
(
constraints.py:410-415). The rows containGenerator_ramp_up_rateand the other three rates. - A ramp limit has a value per snapshot, and the ramp row uses its value at the
later of the two snapshots (
constraints.py:1110-1111). A row is absent at a snapshot where neither the limit nor the start-up or shut-down ramp has a value (constraints.py:1116-1117,1122,1140). - A unit that came in running with a
p_initramps from it into the first snapshot (constraints.py:1149-1151,1105-1109). Wherep_inithas no value, the unit has no ramp row at the first snapshot. - A unit that is not committable has ramp rows with its status fixed at
1(constraints.py:1141-1147), and comes in running whatever itsup_time_before(constraints.py:323). - Each generator's and load's part enters the bus balance with its component's
sign(constraints.py:1480-1481,:1592).
The rung adds a relax bus with a tightened unit that has only start-up and
shut-down ramps. One committable unit has a ramp limit that lifts at the third snapshot, and
a dear committable unit came in running at p_init=40. A fixed unit with ramp limits, a unit of sign -1, a load of sign
1 and a dear backup complete the bus. The row of each unit is tight at
the optimum, and PyPSA solves the rung and its variants to these objectives
(#977):
| network | objective |
|---|---|
| the rung as built | 12862.5 |
the missing limits as 0 in the two partly rows |
36126.875 |
the ramp limit at 0.25 in every snapshot |
14554.75 |
without the p_init |
8437.5 |
| without the fixed unit's ramp limits | 12800.0 |
the unit's sign at 1 |
12592.5 |
the load's sign at -1 |
13887.5 |
| PyPSA | status | note |
|---|---|---|
Generator-com-p-before, -current, -partly-start-up, -partly-shut-down with a missing ramp limit |
done | the four rates are 1 where the limit is missing |
Generator-p-ramp_limit_up, -down per snapshot |
done | Generator_ramp_limit_up and -down over [scenario, snapshot, generator] |
Generator-p-ramp_limit_up, -down at the first snapshot from p_init |
done | Generator_previous_p opens at Generator_status_initial * Generator_p_init |
Generator-p-ramp_limit_up, -down for a unit that is not committable |
done | Generator_ramp_up_allowance and -down have the status fixed at 1 |
Bus-nodal_balance with sign |
done | Generator_sign and Load_sign |
✔
pypsa 1.4.0.post1.dev62+g3625c8168solves this rung's network at objective12862.5, 183 rows.
The network, as PyPSA code
rung_47_linearized_ramps.py
# SPDX-FileCopyrightText: mathspec Contributors
#
# SPDX-License-Identifier: MIT
"""Rung 47: the relaxed file's ramps and signs — tightening at the full build, a ramp limit per snapshot, a `p_init`, a unit that is not committable, and a sign."""
from __future__ import annotations
import math
import spine
PATCH = 'variants/pypsa_linearized_uc.yaml'
OPTIMIZE = {'linearized_unit_commitment': True}
def build():
"""The spine plus a relax bus: five units that each carry one of the rows under review, a feeding load and a dear backup."""
n = spine.build()
n.add('Bus', 'relax')
n.add(
'Generator',
'tight47',
bus='relax',
committable=True,
p_nom=50,
p_min_pu=0.2,
marginal_cost=2,
up_time_before=0,
ramp_limit_start_up=0.5,
ramp_limit_shut_down=0.5,
start_up_cost=10,
shut_down_cost=10,
)
n.add(
'Generator',
'steep47',
bus='relax',
committable=True,
p_nom=40,
marginal_cost=3,
start_up_cost=1,
ramp_limit_up=[0.25, 0.25, math.nan, 0.25],
ramp_limit_down=[0.25, 0.25, 0.25, math.nan],
ramp_limit_start_up=0.25,
)
n.add(
'Generator',
'warm47',
bus='relax',
committable=True,
p_nom=40,
marginal_cost=50,
start_up_cost=1,
p_init=40,
ramp_limit_down=0.25,
ramp_limit_shut_down=0.5,
)
n.add('Generator', 'fixed47', bus='relax', p_nom=60, marginal_cost=1, ramp_limit_up=0.25, ramp_limit_down=0.25)
n.add('Generator', 'sink47', bus='relax', p_nom=20, p_min_pu=0.5, sign=-1)
n.add('Generator', 'backup47', bus='relax', p_nom=300, marginal_cost=500)
n.add('Load', 'feed47', bus='relax', p_set=10, sign=1)
n.add('Load', 'swing47', bus='relax', p_set=[60, 60, 140, 40])
return n
Rung 67 — relaxed link and process commitment#
The keyword relaxes Link and Process as it relaxes Generator
(optimize.py:876-880), and PyPSA builds the four tightening rows for each
(constraints.py:684-792). The rung adds an east bus that three committable
units serve from the north. They are a link and a process, each with a fixed
build and the same start and stop cost, and an extendable link. PyPSA does not
tighten the extendable link, because the tightening rows contain the build as
data (constraints.py:702-704). A dear backup on the east bus balances the
bus.
Each tightening row is tight at the optimum, and PyPSA solves to 47777.33.
When the stop of the link costs 1e-7 more than its start, PyPSA does not
tighten the link, and it solves to 46554.0. When the process is left
untightened in the same way, PyPSA solves to 47465.70. Without the keyword,
it solves to 54013.13.
| PyPSA | status | note |
|---|---|---|
Link-status, -start_up, -shut_down |
done | shares in [0, 1] |
Process-status, -start_up, -shut_down |
done | shares in [0, 1] |
Link-com-p-before, -current, -partly-start-up, -partly-shut-down |
done | a fixed build only |
Process-com-p-before, -current, -partly-start-up, -partly-shut-down |
done | a fixed build only |
Link-com-ext-p-* under the keyword |
done | no tightening for an extendable build |
✔
pypsa 1.4.0.post1.dev62+g3625c8168solves this rung's network at objective47777.33333333333, 194 rows.
The network, as PyPSA code
rung_67_linearized_link_process.py
# SPDX-FileCopyrightText: mathspec Contributors
#
# SPDX-License-Identifier: MIT
"""Rung 67: linearized commitment of a link and a process — both tightened where start and stop cost the same, and an extendable link the tightening skips."""
from __future__ import annotations
import spine
PATCH = 'variants/pypsa_linearized_uc.yaml'
OPTIMIZE = {'linearized_unit_commitment': True}
def build():
"""The spine plus an east bus that committable links and a process serve from the north, and a dear backup there."""
n = spine.build()
n.add('Bus', 'east')
n.add(
'Link',
'tie67',
bus0='north',
bus1='east',
committable=True,
p_nom=60,
p_min_pu=0.3,
marginal_cost=4,
min_up_time=2,
up_time_before=0,
ramp_limit_up=0.4,
ramp_limit_down=0.4,
ramp_limit_start_up=0.5,
ramp_limit_shut_down=0.5,
start_up_cost=30,
shut_down_cost=30,
)
n.add(
'Process',
'conv67',
bus0='north',
bus1='east',
rate0=-1.25,
committable=True,
p_nom=50,
p_min_pu=0.3,
marginal_cost=3,
min_up_time=2,
up_time_before=0,
ramp_limit_up=0.4,
ramp_limit_down=0.4,
ramp_limit_start_up=0.5,
ramp_limit_shut_down=0.5,
start_up_cost=20,
shut_down_cost=20,
)
n.add(
'Link',
'ext67',
bus0='north',
bus1='east',
committable=True,
p_nom_extendable=True,
p_nom_max=30,
capital_cost=5,
p_min_pu=0.2,
marginal_cost=2,
up_time_before=0,
start_up_cost=10,
shut_down_cost=10,
)
n.add('Generator', 'backup67', bus='east', p_nom=200, marginal_cost=400)
n.add('Load', 'east_load', bus='east', p_set=[10, 110, 120, 20])
return n
Rung 74 — a relaxed unit that opens in a later period#
Under the keyword and multi_investment_periods, a committable unit built in
the later period has no output or status before its first snapshot. PyPSA
fills both with zero there (constraints.py:716), so the four tightening rows
open on a unit that was off. The patch reads Generator_previous_p, which is
zero at the first snapshot a unit is active in past the first of the horizon.
The rung's unit has equal start and stop costs, so PyPSA tightens it. It opens
in 2030 and its start-up ramp of 30 is tight at the period's first snapshot.
PyPSA solves to 24825.2778. With the unit built in 2020, PyPSA solves to
3462.5. With a start that costs more than a stop, PyPSA does not tighten
the unit, and it solves to 23226.3333 (#977).
| PyPSA | status | note |
|---|---|---|
Generator-com-p-before, -partly-start-up, -partly-shut-down at a unit's first snapshot past the horizon's first |
done | Generator_previous_p and Generator_previous_status are zero there |
✔
pypsa 1.4.0.post1.dev62+g3625c8168solves this rung's network at objective24825.277777777777, 74 rows.
The network, as PyPSA code
rung_74_linearized_late_opening.py
# SPDX-FileCopyrightText: mathspec Contributors
#
# SPDX-License-Identifier: MIT
"""Rung 74: a relaxed committable unit built in the later period — its tightening rows open on a zero output and a zero status at its first snapshot."""
from __future__ import annotations
from datetime import datetime
import pandas as pd
PATCH = 'variants/pypsa_linearized_uc.yaml'
OPTIMIZE = {'linearized_unit_commitment': True, 'multi_investment_periods': True}
def build():
"""A whole network, not the spine: eight snapshots over two periods, a cheap tightened unit built in 2030, and a dear peaker."""
import pypsa
n = pypsa.Network()
n.snapshots = pd.MultiIndex.from_tuples(
[(2020, datetime(2020, 1, 1, t)) for t in range(4)] + [(2030, datetime(2030, 1, 1, t)) for t in range(4)]
)
n.investment_periods = [2020, 2030]
n.investment_period_weightings['objective'] = [1.0, 0.5]
n.investment_period_weightings['years'] = [10.0, 10.0]
n.snapshot_weightings['objective'] = [2.0, 1.5, 2.5, 2.0, 2.0, 1.5, 2.5, 2.0]
n.add('Bus', 'hub')
n.add('Generator', 'peak74', bus='hub', p_nom=300, marginal_cost=50)
n.add(
'Generator',
'late74',
bus='hub',
committable=True,
p_nom=60,
p_min_pu=0.2,
marginal_cost=2,
start_up_cost=10,
shut_down_cost=10,
ramp_limit_up=0.25,
ramp_limit_down=0.25,
ramp_limit_start_up=0.5,
ramp_limit_shut_down=0.5,
build_year=2030,
lifetime=30,
)
n.add('Load', 'hub_load74', bus='hub', p_set=[40, 60, 70, 50, 50, 60, 55, 20])
return n
The file#
A committable generator, link or process has a status, starts and stops that are shares in [0, 1]. The patch states n.optimize(linearized_unit_commitment=True) over examples/pypsa.yaml, and the shares relax that file's integers (variables.py:91-92). Four rows PyPSA adds only under the keyword tighten the relaxation (constraints.py:684-792). PyPSA builds them only for a unit with a fixed build whose start and stop cost the same in every snapshot and every scenario. The patch states an assumption of no modular committable unit, which PyPSA refuses under the keyword and a tool refuses when data is attached. Every other row is the integer run's.
Generator-status#
Generator_status
Generator_status:
description: >-
`Generator-status` — how much of a committable unit is on, a share in [0, 1]
rather than an integer: the relaxation the keyword solves
(`variables.py:91-92`)
domain: null
bounds:
upper: 1
Generator-start_up#
Generator_start_up
Generator_start_up:
description: "`Generator-start_up` — how much of a committable unit turns on this snapshot, a share in [0, 1] (`variables.py:142-143`)"
domain: null
bounds:
upper: 1
Generator-shut_down#
Generator_shut_down
Generator_shut_down:
description: "`Generator-shut_down` — how much of a committable unit turns off this snapshot, a share in [0, 1] (`variables.py:193-194`)"
domain: null
bounds:
upper: 1
Link-status#
Link_status
Link_status:
description: >-
`Link-status` — how much of a committable link is on, a share in [0, 1]
rather than an integer: the relaxation the keyword solves
(`variables.py:91-92`)
domain: null
bounds:
upper: 1
Link-start_up#
Link_start_up
Link_start_up:
description: "`Link-start_up` — how much of a committable link turns on this snapshot, a share in [0, 1] (`variables.py:142-143`)"
domain: null
bounds:
upper: 1
Link-shut_down#
Link_shut_down
Link_shut_down:
description: "`Link-shut_down` — how much of a committable link turns off this snapshot, a share in [0, 1] (`variables.py:193-194`)"
domain: null
bounds:
upper: 1
Process-status#
Process_status
Process_status:
description: >-
`Process-status` — how much of a committable process is on, a share in [0, 1]
rather than an integer: the relaxation the keyword solves
(`variables.py:91-92`)
domain: null
bounds:
upper: 1
Process-start_up#
Process_start_up
Process_start_up:
description: "`Process-start_up` — how much of a committable process turns on this snapshot, a share in [0, 1] (`variables.py:142-143`)"
domain: null
bounds:
upper: 1
Process-shut_down#
Process_shut_down
Process_shut_down:
description: "`Process-shut_down` — how much of a committable process turns off this snapshot, a share in [0, 1] (`variables.py:193-194`)"
domain: null
bounds:
upper: 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 (`constraints.py:729-742`). The share it
is shutting down counts only at the shut-down ramp. At the first snapshot,
the unit carries in the state and output it brought into the horizon,
and has no row where it came in running without an initial output
(`constraints.py:712-719`)
dims: [scenario, snapshot, generator]
where: Generator_committable AND NOT Generator_p_nom_extendable AND count(count(Generator_start_up_cost != Generator_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Generator_active AND (position(snapshot) > 0 OR Generator_status_initial == 0 OR Generator_p_init)
expression: >-
Generator_previous_p
- Generator_shut_down_rate * Generator_p_nom * Generator_previous_status
- (Generator_p_max_pu * Generator_p_nom - Generator_shut_down_rate * 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 (`constraints.py:744-757`)
dims: [scenario, snapshot, generator]
where: Generator_committable AND NOT Generator_p_nom_extendable AND count(count(Generator_start_up_cost != Generator_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Generator_active
expression: >-
Generator_p - Generator_p_max_pu * Generator_p_nom * Generator_status
+ (Generator_p_max_pu * Generator_p_nom - Generator_start_up_rate * 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 two ramps (`constraints.py:759-774`). They are
the ramp of the share on and the start-up ramp of the share coming on
dims: [scenario, snapshot, generator]
where: Generator_committable AND NOT Generator_p_nom_extendable AND count(count(Generator_start_up_cost != Generator_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Generator_active AND (position(snapshot) > 0 OR Generator_status_initial == 0 OR Generator_p_init)
expression: >-
Generator_p - Generator_previous_p
- (Generator_p_min_pu * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom) * Generator_status
+ Generator_p_min_pu * Generator_p_nom * Generator_previous_status
+ (Generator_p_min_pu * Generator_p_nom + Generator_ramp_up_rate * Generator_p_nom - Generator_start_up_rate * Generator_p_nom)
* Generator_start_up <= 0
Generator-com-partly-shut-down#
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 (`constraints.py:776-791`)
dims: [scenario, snapshot, generator]
where: Generator_committable AND NOT Generator_p_nom_extendable AND count(count(Generator_start_up_cost != Generator_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Generator_active AND (position(snapshot) > 0 OR Generator_status_initial == 0 OR Generator_p_init)
expression: >-
Generator_previous_p - Generator_p
- Generator_shut_down_rate * Generator_p_nom * Generator_previous_status
+ (Generator_shut_down_rate * Generator_p_nom - Generator_ramp_down_rate * Generator_p_nom) * Generator_status
- (Generator_p_min_pu * Generator_p_nom + Generator_ramp_down_rate * Generator_p_nom - Generator_shut_down_rate * Generator_p_nom)
* Generator_start_up <= 0
Link-com-p-before#
Link_com_p_before
Link_com_p_before:
description: >-
`Link-com-p-before` — the output a link had entering this snapshot fits
the share of it still on (`constraints.py:729-742`). The share it is
shutting down counts only at the shut-down ramp. At the first snapshot,
the link carries in the state and output it brought into the horizon,
and has no row where it came in running without an initial output
(`constraints.py:712-719`)
dims: [scenario, snapshot, link]
where: Link_committable AND NOT Link_p_nom_extendable AND count(count(Link_start_up_cost != Link_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Link_active AND (position(snapshot) > 0 OR Link_status_initial == 0 OR Link_p_init)
expression: >-
Link_previous_p
- Link_shut_down_rate * Link_p_nom * Link_previous_status
- (Link_p_max_pu * Link_p_nom - Link_shut_down_rate * Link_p_nom)
* (Link_status - Link_start_up) <= 0
Link-com-p-current#
Link_com_p_current
Link_com_p_current:
description: >-
`Link-com-p-current` — output fits the share on, and the share starting
up only up to the start-up ramp (`constraints.py:744-757`)
dims: [scenario, snapshot, link]
where: Link_committable AND NOT Link_p_nom_extendable AND count(count(Link_start_up_cost != Link_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Link_active
expression: >-
Link_p - Link_p_max_pu * Link_p_nom * Link_status
+ (Link_p_max_pu * Link_p_nom - Link_start_up_rate * Link_p_nom) * Link_start_up <= 0
Link-com-partly-start-up#
Link_com_partly_start_up
Link_com_partly_start_up:
description: >-
`Link-com-partly-start-up` — raising output while a share is starting up
is bounded by two ramps (`constraints.py:759-774`). They are the ramp of
the share on and the start-up ramp of the share coming on
dims: [scenario, snapshot, link]
where: Link_committable AND NOT Link_p_nom_extendable AND count(count(Link_start_up_cost != Link_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Link_active AND (position(snapshot) > 0 OR Link_status_initial == 0 OR Link_p_init)
expression: >-
Link_p - Link_previous_p
- (Link_p_min_pu * Link_p_nom + Link_ramp_up_rate * Link_p_nom) * Link_status
+ Link_p_min_pu * Link_p_nom * Link_previous_status
+ (Link_p_min_pu * Link_p_nom + Link_ramp_up_rate * Link_p_nom - Link_start_up_rate * Link_p_nom)
* Link_start_up <= 0
Link-com-partly-shut-down#
Link_com_partly_shut_down
Link_com_partly_shut_down:
description: >-
`Link-com-partly-shut-down` — lowering output while a share is shutting
down is bounded likewise, by the shut-down ramp (`constraints.py:776-791`)
dims: [scenario, snapshot, link]
where: Link_committable AND NOT Link_p_nom_extendable AND count(count(Link_start_up_cost != Link_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Link_active AND (position(snapshot) > 0 OR Link_status_initial == 0 OR Link_p_init)
expression: >-
Link_previous_p - Link_p
- Link_shut_down_rate * Link_p_nom * Link_previous_status
+ (Link_shut_down_rate * Link_p_nom - Link_ramp_down_rate * Link_p_nom) * Link_status
- (Link_p_min_pu * Link_p_nom + Link_ramp_down_rate * Link_p_nom - Link_shut_down_rate * Link_p_nom)
* Link_start_up <= 0
Process-com-p-before#
Process_com_p_before
Process_com_p_before:
description: >-
`Process-com-p-before` — the output a process had entering this snapshot
fits the share of it still on (`constraints.py:729-742`). The share it
is shutting down counts only at the shut-down ramp. At the first snapshot,
the process carries in the state and output it brought into the horizon,
and has no row where it came in running without an initial output
(`constraints.py:712-719`)
dims: [scenario, snapshot, process]
where: Process_committable AND NOT Process_p_nom_extendable AND count(count(Process_start_up_cost != Process_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Process_active AND (position(snapshot) > 0 OR Process_status_initial == 0 OR Process_p_init)
expression: >-
Process_previous_p
- Process_shut_down_rate * Process_p_nom * Process_previous_status
- (Process_p_max_pu * Process_p_nom - Process_shut_down_rate * Process_p_nom)
* (Process_status - Process_start_up) <= 0
Process-com-p-current#
Process_com_p_current
Process_com_p_current:
description: >-
`Process-com-p-current` — output fits the share on, and the share starting
up only up to the start-up ramp (`constraints.py:744-757`)
dims: [scenario, snapshot, process]
where: Process_committable AND NOT Process_p_nom_extendable AND count(count(Process_start_up_cost != Process_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Process_active
expression: >-
Process_p - Process_p_max_pu * Process_p_nom * Process_status
+ (Process_p_max_pu * Process_p_nom - Process_start_up_rate * Process_p_nom) * Process_start_up <= 0
Process-com-partly-start-up#
Process_com_partly_start_up
Process_com_partly_start_up:
description: >-
`Process-com-partly-start-up` — raising output while a share is starting
up is bounded by two ramps (`constraints.py:759-774`). They are the ramp
of the share on and the start-up ramp of the share coming on
dims: [scenario, snapshot, process]
where: Process_committable AND NOT Process_p_nom_extendable AND count(count(Process_start_up_cost != Process_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Process_active AND (position(snapshot) > 0 OR Process_status_initial == 0 OR Process_p_init)
expression: >-
Process_p - Process_previous_p
- (Process_p_min_pu * Process_p_nom + Process_ramp_up_rate * Process_p_nom) * Process_status
+ Process_p_min_pu * Process_p_nom * Process_previous_status
+ (Process_p_min_pu * Process_p_nom + Process_ramp_up_rate * Process_p_nom - Process_start_up_rate * Process_p_nom)
* Process_start_up <= 0
Process-com-partly-shut-down#
Process_com_partly_shut_down
Process_com_partly_shut_down:
description: >-
`Process-com-partly-shut-down` — lowering output while a share is shutting
down is bounded likewise, by the shut-down ramp (`constraints.py:776-791`)
dims: [scenario, snapshot, process]
where: Process_committable AND NOT Process_p_nom_extendable AND count(count(Process_start_up_cost != Process_shut_down_cost, over=snapshot) > 0, over=scenario) == 0 AND Process_active AND (position(snapshot) > 0 OR Process_status_initial == 0 OR Process_p_init)
expression: >-
Process_previous_p - Process_p
- Process_shut_down_rate * Process_p_nom * Process_previous_status
+ (Process_shut_down_rate * Process_p_nom - Process_ramp_down_rate * Process_p_nom) * Process_status
- (Process_p_min_pu * Process_p_nom + Process_ramp_down_rate * Process_p_nom - Process_shut_down_rate * Process_p_nom)
* Process_start_up <= 0
Generator_marginal_cost_incremental#
Generator_marginal_cost_incremental
Generator_marginal_cost_incremental:
description: >-
`Generator-marginal_cost_piecewise` — the running cost of a generator
with a curve is at least the curve at its output, stated by segments
filled in order, where linopy chooses the incremental rows. No status
gates it, because no generator with a curve is committable under the
keyword
activity: null
Generator_marginal_cost_curve_is_not_committable#
Generator_marginal_cost_curve_is_not_committable
Generator_marginal_cost_curve_is_not_committable:
holds: "NOT Generator_committable"
where: "Generator_marginal_cost_curve"
description: >-
a generator with a piecewise `marginal_cost` curve is not committable.
PyPSA gates such a curve by the status share (`optimize.py:314-318`,
`piecewise.py:200-210`), and a curve gate is binary or integer
Generator_committable_is_not_modular#
Generator_committable_is_not_modular
Generator_committable_is_not_modular:
holds: "NOT (Generator_p_nom_mod > 0)"
where: "Generator_committable AND count(Generator_active, over=snapshot) > 0"
description: >-
a modular committable unit counts its status in modules, which the
relaxation cannot share out. PyPSA refuses it under the keyword
(`optimize.py:841-842`, `consistency.py:1588-1629`)
Link_committable_is_not_modular#
Link_committable_is_not_modular
Link_committable_is_not_modular:
holds: "NOT (Link_p_nom_mod > 0)"
where: "Link_committable AND count(Link_active, over=snapshot) > 0"
description: >-
a modular committable unit counts its status in modules, which the
relaxation cannot share out. PyPSA refuses it under the keyword
(`optimize.py:841-842`, `consistency.py:1588-1629`)
Process_committable_is_not_modular#
Process_committable_is_not_modular
Process_committable_is_not_modular:
holds: "NOT (Process_p_nom_mod > 0)"
where: "Process_committable AND count(Process_active, over=snapshot) > 0"
description: >-
a modular committable unit counts its status in modules, which the
relaxation cannot share out. PyPSA refuses it under the keyword
(`optimize.py:841-842`, `consistency.py:1588-1629`)
See also
- PyPSA in one file, for the base spec and every rung
override, for how a patch is laid over its base- Patch, for the term