piecewise_ragged¶
Per-generator cost curves of different lengths, each as long as its own data.
✔ Agrees with hand-written linopy 0.9.0 — objective 426, matched to
rtol=1e-09.
The problem¶
A breakpoint dimension is one axis for the whole system, but a curve is per unit. The hydro unit here has two breakpoints, the coal plant three, the gas turbine four. Nothing in the model has to know which is longest.
The weights run over \(\mathcal{K}_g\), the curve's own breakpoint set, which
is what points: bp_x says. Without it the shorter curves have to be padded
out to the longest. Padding buys a weight per unused breakpoint, and
method: convex refuses it, because a repeated point is not a strictly
increasing breakpoint.
The same model, as math
Least-cost dispatch where each generator's cost curve has as many breakpoints as its data gives it — two for the hydro unit, four for the gas turbine — rather than as many as the longest curve in the system.
Sets¶
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) — snapshot — dispatch periods |
| \(\mathcal{G}\) | index \(g\) — generator — dispatchable units |
| \(\mathcal{B}\) | index \(b\) — bp — breakpoints, as many as the longest curve needs |
Parameters¶
| Symbol | Meaning |
|---|---|
| \(\mathrm{p}^{\mathrm{max}}\) | p_max over \(\mathcal{G}\) — maximum dispatch |
| \(\mathrm{load}\) | load over \(\mathcal{T}\) — demand to be met |
| \(\mathrm{bp\_x}\) | bp_x over \(\mathcal{G} \times \mathcal{B}\) — breakpoint dispatch levels, one curve per generator and no two the same length |
| \(\mathrm{bp\_y}\) | bp_y over \(\mathcal{G} \times \mathcal{B}\) — cost at each breakpoint |
Variables¶
| Symbol | Meaning |
|---|---|
| \(p\) | p over \(\mathcal{T} \times \mathcal{G}\) — dispatched power |
| \(\mathit{op\_cost}\) | op_cost over \(\mathcal{T} \times \mathcal{G}\) — operating cost, piecewise-linear in dispatch |
Upright is what the data supplies — a parameter such as \(\mathrm{p}^{\mathrm{max}}\), a coordinate map, a label — and italic is what the solver chooses, such as \(p\). An index is italic too, being what a quantifier chooses, and a set is script.
\(t \boxminus_{v} k\) denotes translation with \(v\) standing where index \(t-k\) leaves the dimension (shift(edge=v)), so the row at that boundary is built and carries \(v\) rather than being dropped.
Objective¶
Subject to¶
balance
cost_curve
Variable domains¶
p
op_cost
Assumptions¶
cost_curve_complete
cost_curve_increasing
cost_curve_curvature
cost_curve_contiguous
The tabs start from the instance's tables — one frame per parameter.
description: >-
Least-cost dispatch where each generator's cost curve has as many breakpoints
as its data gives it — two for the hydro unit, four for the gas turbine —
rather than as many as the longest curve in the system.
dimensions:
snapshot:
description: dispatch periods
dtype: int
generator:
description: dispatchable units
dtype: str
bp:
description: breakpoints, as many as the longest curve needs
dtype: int
parameters:
p_max:
description: maximum dispatch
dims: [generator]
load:
description: demand to be met
dims: [snapshot]
bp_x:
description: breakpoint dispatch levels, one curve per generator and no two the same length
dims: [generator, bp]
bp_y:
description: cost at each breakpoint
dims: [generator, bp]
variables:
p:
description: dispatched power
dims: [snapshot, generator]
bounds:
lower: 0
upper: p_max
op_cost:
description: operating cost, piecewise-linear in dispatch
dims: [snapshot, generator]
bounds:
lower: 0
piecewise:
cost_curve:
description: >-
each unit's own curve — `points: bp_x` says a curve runs as far as its own
breakpoints do, so the hydro unit pays for two weights and the gas turbine
for four
over: bp
points: bp_x
links:
- [p, bp_x]
- [op_cost, bp_y, ">="]
method: convex
constraints:
balance:
description: every period's demand is met
dims: [snapshot]
expression: sum(p, over=generator) == load
objective:
sense: minimize
description: total operating cost, taken off each unit's own curve
expression: sum(sum(op_cost, over=generator), over=snapshot)
The model-building half of examples/ports/references/linopy/piecewise_ragged.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']
load: pd.Series = tables['load'].set_index('snapshot')['value']
reach = tables['bp_x'].groupby('generator')['value'].max().reindex(p_max.index)
m = linopy.Model()
p = m.add_variables(lower=0, upper=reach, coords=[load.index, p_max.index], name='p')
op_cost = m.add_variables(lower=0, coords=[load.index, p_max.index], name='op_cost')
for g, lines in segments(tables).items():
for k, (slope, intercept) in enumerate(lines):
m.add_constraints(
op_cost.sel(generator=g) - slope * p.sel(generator=g) >= intercept,
name=f'chord_{g}_{k}',
)
m.add_constraints(p.sum('generator') == load, name='balance')
m.add_objective(op_cost.sum())
return m
What it exercises¶
points: is what makes the curve its own. The weights, and the segment
binaries where a method declares them, exist only where the mask does, so the
hydro unit carries two rather than four. No value is asked for at a breakpoint
the curve leaves out. A gap in the middle of a curve is refused when data
attaches: the marked breakpoints have to follow one another, though they need
not start at the head of the axis.
The linopy reference reaches the same optimum from the other formulation: segment lines rather than weights, which is exact for a convex curve under minimisation and needs no auxiliary variable. Two formulations agreeing is a stronger check than two spellings of one. Both sides stay a pure LP, so this model keeps its duals.
examples/piecewise_ragged.yaml · back to all models