multi_period¶
Capacity decided once per investment period, binding at every snapshot inside it — and the periods need not be the same size.
The problem¶
Two dimensions cannot state this at the resolution a real study wants.
period × snapshot is a rectangle, so every period gets the same number of
snapshots — and a study that models 2030 hourly and 2050 in four-hour blocks is
asking for exactly the opposite.
So snapshot is one flat dimension carrying \(\mathrm{period}\) as a
coordinate, the same way generator carries
\(\mathrm{bus}\) in transport. Ragged periods then cost nothing:
a coordinate is a per-row column, and four snapshots in 2030 beside two in 2050
is just a column with four of one value and two of another.
Both directions of one mapping¶
Grouping reads the coordinate one way:
group_sum(p, over=snapshot, by=period) is a per-period CO₂ budget, and
monthly_budget is the same construct on a different
coordinate.
within_cap reads it the other way. Capacity lives on period and binds at
each snapshot, so a coarse quantity is pulled onto a fine one:
at and group_sum take the same two arguments because (over, by) names one
mapping table and the helper says which direction it is walked.
A per-period parameter needs neither: data prep can join it onto the
snapshot index before the model sees it. p_nom is a variable, and a
variable is not data to be joined — which is the line between the two, and why
the pullback is a construct in the language rather than a step before it.
The same model, as math
Sets¶
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) --- snapshot with \(\mathrm{period}: \mathcal{T} \to \mathcal{E}\) |
| \(\mathcal{E}\) | index \(e\) --- period |
| \(\mathcal{G}\) | index \(g\) --- generator |
Parameters¶
| Symbol | Meaning |
|---|---|
| \(\mathit{load}\) | load over \(\mathcal{T}\) |
| \(\mathit{weight}\) | weight over \(\mathcal{T}\) |
| \(\mathit{opex}\) | opex over \(\mathcal{G}\) |
| \(\mathit{capex}\) | capex over \(\mathcal{G} \times \mathcal{E}\) |
Variables¶
| Symbol | Meaning |
|---|---|
| \(p\) | p over \(\mathcal{T} \times \mathcal{G}\) |
| \(p^{\mathrm{nom}}\) | p_nom over \(\mathcal{E} \times \mathcal{G}\) |
Objective¶
total_cost
Subject to¶
within_cap
balance
Variable domains¶
p
p_nom
# Multi-period investment on a *flat* snapshot index.
#
# `snapshot` is one dimension carrying `period` as a coordinate, rather than a
# `period x snapshot` rectangle. That is what lets the periods be **ragged** —
# four snapshots in 2030 and two in 2050 here — because a coordinate is a
# per-row column and nothing assumes every period has the same shape. A
# rectangle cannot say it: it forces one resolution on every period.
#
# `within_cap` reads the mapping the other way round from a grouping: capacity
# is decided per period and binds at every snapshot in that period, so a
# coarse-dim quantity is read at a fine-dim row. A per-period *parameter* could
# be pre-joined in data prep; `p_nom` is a variable, and a variable is not data
# to be joined.
dimensions:
snapshot:
dtype: int
coords: [period]
period:
dtype: int
generator:
dtype: str
parameters:
load:
dims: [snapshot]
# what one snapshot stands for: a 2050 snapshot represents four hours, so the
# operating cost of a coarse period is not understated against a fine one
weight:
dims: [snapshot]
opex:
dims: [generator]
capex:
dims: [generator, period]
variables:
p:
foreach: [snapshot, generator]
bounds:
lower: 0
p_nom:
foreach: [period, generator]
bounds:
lower: 0
upper: 100
constraints:
within_cap:
foreach: [snapshot, generator]
expression: p <= at(p_nom, onto=snapshot, by=period)
balance:
foreach: [snapshot]
expression: sum(p, over=generator) == load
objectives:
total_cost:
sense: minimize
expression: p * opex * weight + p_nom * capex
Reading the answer¶
Costs are chosen so each period picks a different technology, which is what makes the per-period capacity visible rather than incidental:
| period | wind | gas |
|---|---|---|
| 2030 | 20 | 10 |
| 2050 | 60 | 0 |
2030 peaks at 30 and splits the build — wind is dearer to install but free to run. 2050 peaks at 60 with every snapshot weighted four times, so the operating term dominates and the whole build goes to wind. Objective 750.0, agreed integer for integer by both lanes.
The weights are the reason the two periods are comparable at all: a coarse snapshot standing for four hours contributes four hours of operating cost, so a period is not made cheap by being modelled coarsely.