storage¶
Dispatch plus a battery that carries its charge from one snapshot to the next, closed into a cycle over the horizon.
✔ Agrees with hand-written linopy 0.10.0 — objective 5650, matched to
rtol=1e-09.
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
model writes the charging efficiency as the literal 0.9 rather than
declaring a parameter, so it appears here as a number and not as an \(\eta\).
The model¶
The same model, as math
Dispatch plus a battery whose state of charge is closed into a cycle: the horizon ends where it began.
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 a generator in a snapshot |
| \(\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¶
Subject to¶
power_balance
soc_balance
Variable domains¶
p
charge
discharge
soc
The tabs start from the instance's tables — one frame per parameter.
description: >-
Dispatch plus a battery whose state of charge is closed into a cycle: the
horizon ends where it began.
dimensions:
snapshot:
description: dispatch periods, cyclic at the horizon
dtype: int
generator:
description: generating units
dtype: str
parameters:
p_max:
description: installed capacity
dims: [generator]
cost:
description: marginal cost
dims: [generator]
load:
description: demand to be met
dims: [snapshot]
variables:
p:
description: output of a generator in a snapshot
dims: [snapshot, generator]
bounds:
lower: 0
upper: p_max
charge:
description: energy into the store
dims: [snapshot]
bounds:
lower: 0
upper: 30
discharge:
description: energy out of the store
dims: [snapshot]
bounds:
lower: 0
upper: 30
soc:
description: state of charge carried into the next snapshot
dims: [snapshot]
bounds:
lower: 0
upper: 100
constraints:
power_balance:
description: generation plus what the store gives back covers the load, net of charging
dims: [snapshot]
expression: sum(p, over=generator) + discharge - charge == load
soc_balance:
description: >-
the level carried out of a snapshot is the one carried in plus what was
stored, minus what was taken — and it wraps at the horizon, so the first
snapshot inherits from the last
dims: [snapshot]
expression: soc == shift(soc, along=snapshot, offset=1, edge='wrap') + charge * 0.9 - discharge
objective:
sense: minimize
description: total cost of generation; storing and releasing energy is free here
expression: sum(p * cost)
The model-building half of examples/ports/references/linopy/storage.py:
def build(tables: dict[str, pd.DataFrame]) -> linopy.Model:
"""The instance's tables as a linopy model, row for row.
``tables`` is the same mapping the specsolve call attaches as ``sources``.
"""
p_max: pd.Series = tables['p_max'].set_index('generator')['value']
cost: pd.Series = tables['cost'].set_index('generator')['value']
load: pd.Series = tables['load'].set_index('snapshot')['value']
snapshots = load.index
m = linopy.Model()
p = m.add_variables(lower=0, upper=p_max, coords=[snapshots, p_max.index], name='p')
charge = m.add_variables(lower=0, upper=30, coords=[snapshots], name='charge')
discharge = m.add_variables(lower=0, upper=30, coords=[snapshots], name='discharge')
soc = m.add_variables(lower=0, upper=100, coords=[snapshots], name='soc')
m.add_constraints(p.sum('generator') + discharge - charge == load, name='power_balance')
m.add_constraints(soc == soc.roll(snapshot=1) + 0.9 * charge - discharge, name='soc_balance')
m.add_objective((p * cost).sum())
return m
examples/storage.yaml · back to all models