dispatch¶
Least-cost generation against a load profile — the smallest model that is still a model.
✔ Agrees with hand-written linopy 0.9.0 — objective 10500, matched to
rtol=1e-09.
The problem¶
Pick an output \(p_{s,g}\) for every generator in every snapshot, so that the fleet meets the load exactly and costs as little as possible:
The model¶
The same model, as math
Least-cost dispatch of a generator fleet against an hourly load.
Sets¶
| Symbol | Meaning |
|---|---|
| \(\mathcal{S}\) | index \(s\) --- snapshot --- dispatch periods |
| \(\mathcal{G}\) | index \(g\) --- generator --- generating units |
Parameters¶
| Symbol | Meaning |
|---|---|
| \(\bar p\) | p_max over \(\mathcal{G}\) --- installed capacity |
| \(\ell\) | load over \(\mathcal{S}\) --- demand to be met |
| \(c\) | cost over \(\mathcal{G}\) --- marginal cost |
Variables¶
| Symbol | Meaning |
|---|---|
| \(p\) | p over \(\mathcal{S} \times \mathcal{G}\) --- output of a generator in a snapshot |
Objective¶
Subject to¶
power_balance
Variable domains¶
p
The tabs start from the instance’s tables — one frame per parameter.
description: Least-cost dispatch of a generator fleet against an hourly load.
dimensions:
snapshot:
description: dispatch periods
dtype: int
generator:
description: generating units
values: [wind, solar, gas]
parameters:
p_max:
description: installed capacity
dims: [generator]
load:
description: demand to be met
dims: [snapshot]
cost:
description: marginal cost
dims: [generator]
variables:
p:
description: output of a generator in a snapshot
foreach: [snapshot, generator]
where: "p_max > 0"
bounds:
lower: 0
upper: p_max
constraints:
power_balance:
foreach: [snapshot]
expression: sum(p, over=generator) == load
objective:
sense: minimize
description: total cost of generation over the horizon
expression: p * cost
The model-building half of examples/ports/references/linopy/dispatch.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 lpspec call binds 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']
m = linopy.Model()
p = m.add_variables(lower=0, upper=p_max, coords=[load.index, p_max.index], name='p')
m.add_constraints(p.sum('generator') == load, name='power_balance')
m.add_objective((p * cost).sum())
return m
What it exercises¶
where: "p_max > 0" is the one line worth pausing on. A generator with no
capacity gets no columns at all — not a column pinned to zero — so a
retired unit costs nothing to carry in the data. That is row absence, and it
is how sparsity is spelled throughout: see where in the
language reference.
examples/dispatch.yaml · back to all models