storage¶
Dispatch plus a battery, and the only construct in the language whose cost is not obviously linear.
The problem¶
State of charge links each snapshot to the one before it, cyclically:
with \(s-1\) wrapping at the horizon, so the battery ends where it started. The
charging efficiency is written into the model as a literal 0.9 rather than
declared as a parameter โ which is why it appears as a number here and not as
an \(\eta\).
The model¶
The same model, as math
Sets¶
| Symbol | Meaning |
|---|---|
| \(\mathcal{S}\) | index \(s\) --- snapshot --- dispatch periods, cyclic at the horizon |
| \(\mathcal{G}\) | index \(g\) --- generator --- generating units |
Parameters¶
| Symbol | Meaning |
|---|---|
| \(\bar p\) | p_max over \(\mathcal{G}\) --- installed capacity |
| \(c\) | cost over \(\mathcal{G}\) --- marginal cost |
| \(\ell\) | load over \(\mathcal{S}\) --- demand to be met |
Variables¶
| Symbol | Meaning |
|---|---|
| \(p\) | p over \(\mathcal{S} \times \mathcal{G}\) --- output of generator \(g\) in snapshot \(s\) |
| \(\mathrm{charge}\) | charge over \(\mathcal{S}\) --- energy into the store |
| \(\mathrm{discharge}\) | discharge over \(\mathcal{S}\) --- energy out of the store |
| \(\mathrm{soc}\) | soc over \(\mathcal{S}\) --- state of charge carried into the next snapshot |
\(t \ominus k\) denotes cyclic translation: index \(t-k\) taken modulo the size of the dimension (roll). Plain \(t-k\) (shift) has no wraparound --- terms translated past the edge are simply absent.
Objective¶
total_cost
Subject to¶
power_balance
soc_balance
Variable domains¶
p
charge
discharge
soc
dimensions:
snapshot:
dtype: int
generator:
dtype: str
parameters:
p_max:
dims: [generator]
cost:
dims: [generator]
load:
dims: [snapshot]
variables:
p:
foreach: [snapshot, generator]
bounds:
lower: 0
upper: p_max
charge:
foreach: [snapshot]
bounds:
lower: 0
upper: 30
discharge:
foreach: [snapshot]
bounds:
lower: 0
upper: 30
soc:
foreach: [snapshot]
bounds:
lower: 0
upper: 100
constraints:
power_balance:
foreach: [snapshot]
expression: sum(p, over=generator) + discharge - charge == load
soc_balance:
foreach: [snapshot]
# cyclic storage: soc wraps around the snapshot horizon
expression: soc == shift(soc, over=snapshot, by=1, edge='wrap') + charge * 0.9 - discharge
objectives:
total_cost:
sense: minimize
expression: p * cost
What it exercises¶
shift(soc, over=snapshot, by=1, edge='wrap') is the whole of it. One term reaches one position
back along snapshot, and edge='wrap' wraps โ the first snapshot reads the
last, which is what makes the storage cyclic without a boundary condition
written out by hand. Omitting edge= is the same node without the wrap, where
positions translated past the edge simply contribute nothing.
It is also the one plan shape whose cost is not obviously linear in the model size, which is why it is named in Not measured yet in the benchmarks.
examples/storage.yaml ยท back to all models