piecewise_conversion¶
Converters whose flows share one curve, where how many flows is data.
✔ Agrees with hand-written linopy 0.9.0 — objective 5990, matched to
rtol=1e-06.
The problem¶
A converter ties its flows together through one operating point: a boiler burns fuel to make heat, a CHP (combined heat and power) unit burns fuel to make heat and power. The curve is one convex combination per converter. How many flows it ties is a property of the system, not of the file: two here, three there, and a unit with a fourth is a row in a table.
piecewise: cannot say that: its links:
are a list, so the arity would have to be written out. The formulation it
would have emitted is four ordinary declarations:
| what | how |
|---|---|
| the weights | a variable over [converter, time, bp], masked by where: bp_present |
| on one segment | sos: type: 2 over that variable |
| one operating point | sum(weight, over=bp) == 1 |
| the tie | one row per flow, reading its converter's weights through at(…, by=converter_of, over=converter, into=flow) |
The same model, as math
Least-cost heat and power from two converters whose flows are tied to one piecewise curve each — a boiler tying two flows, a CHP unit tying three, and neither number written anywhere in the file.
Sets¶
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) — time — dispatch periods |
| \(\mathcal{C}\) | index \(c\) — converter with \(\mathrm{converter\_of}: \mathcal{F} \to \mathcal{C}\) — units converting one carrier into others |
| \(\mathcal{F}\) | index \(f\) — flow with \(\mathrm{converter\_of}: \mathcal{F} \to \mathcal{C}\) — a converter's inputs and outputs, one row each |
| \(\mathcal{B}\) | index \(b\) — bp — breakpoints, as many as the longest curve needs |
Parameters¶
| Symbol | Meaning |
|---|---|
| \(\mathrm{bp\_rate}\) | bp_rate over \(\mathcal{F} \times \mathcal{B}\) — what each flow runs at, at each breakpoint of its converter's curve |
| \(\mathrm{bp\_present}\) | bp_present over \(\mathcal{C} \times \mathcal{B}\) — how far each converter's curve runs |
| \(\mathrm{rate}^{\mathrm{max}}\) | rate_max over \(\mathcal{F}\) — what each flow runs at when its converter is at its last breakpoint |
| \(\mathrm{is\_heat}\) | is_heat over \(\mathcal{F}\) — which flows deliver heat |
| \(\mathrm{is\_power}\) | is_power over \(\mathcal{F}\) — which flows deliver power |
| \(\mathrm{fuel\_price}\) | fuel_price over \(\mathcal{F}\) — what a unit of each input flow costs |
| \(\mathrm{heat\_demand}\) | heat_demand over \(\mathcal{T}\) — heat to be delivered |
| \(\mathrm{power\_demand}\) | power_demand over \(\mathcal{T}\) — power to be delivered |
Variables¶
| Symbol | Meaning |
|---|---|
| \(\mathit{rate}\) | rate over \(\mathcal{F} \times \mathcal{T}\) — what each flow runs at |
| \(\mathit{weight}\) | weight over \(\mathcal{C} \times \mathcal{T} \times \mathcal{B}\) — how much of each breakpoint the converter's operating point is made of — one convex combination per converter and period, over the breakpoints its own curve runs to |
Upright is what the data supplies — a parameter such as \(\mathrm{bp\_rate}\), a coordinate map, a label — and italic is what the solver chooses, such as \(\mathit{rate}\). An index is italic too, being what a quantifier chooses, and a set is script.
Objective¶
Subject to¶
one_operating_point
on_the_curve
heat_balance
power_balance
Variable domains¶
rate
weight
on_one_segment
The tabs start from the instance's tables — one frame per parameter.
description: >-
Least-cost heat and power from two converters whose flows are tied to one
piecewise curve each — a boiler tying two flows, a CHP unit tying three, and
neither number written anywhere in the file.
dimensions:
time:
description: dispatch periods
dtype: int
converter:
description: units converting one carrier into others
dtype: str
flow:
description: a converter's inputs and outputs, one row each
dtype: str
bp:
description: breakpoints, as many as the longest curve needs
dtype: int
relations:
converter_of:
description: which converter a flow belongs to
key: flow
values: converter
parameters:
bp_rate:
description: what each flow runs at, at each breakpoint of its converter's curve
dims: [flow, bp]
bp_present:
description: how far each converter's curve runs
dims: [converter, bp]
dtype: bool
rate_max:
description: what each flow runs at when its converter is at its last breakpoint
dims: [flow]
is_heat:
description: which flows deliver heat
dims: [flow]
is_power:
description: which flows deliver power
dims: [flow]
fuel_price:
description: what a unit of each input flow costs
dims: [flow]
heat_demand:
description: heat to be delivered
dims: [time]
power_demand:
description: power to be delivered
dims: [time]
variables:
rate:
description: what each flow runs at
dims: [flow, time]
bounds:
lower: 0
upper: rate_max
weight:
description: >-
how much of each breakpoint the converter's operating point is made of —
one convex combination per converter and period, over the breakpoints its
own curve runs to
dims: [converter, time, bp]
where: bp_present
bounds:
lower: 0
upper: 1
sos:
on_one_segment:
description: >-
at most two weights, and those two neighbours — which is what puts the
operating point on a segment of the curve rather than anywhere in its hull
variable: weight
along: bp
type: 2
constraints:
one_operating_point:
description: each converter sits somewhere on its curve, in every period
dims: [converter, time]
expression: sum(weight, over=bp) == 1
on_the_curve:
description: every flow reads its own value off its converter's weights
dims: [flow, time]
expression: rate == sum(at(weight, by=converter_of, over=converter, into=flow) * bp_rate, over=bp)
heat_balance:
description: heat delivered meets the demand
dims: [time]
expression: sum(rate * is_heat, over=flow) == heat_demand
power_balance:
description: power delivered meets the demand
dims: [time]
expression: sum(rate * is_power, over=flow) == power_demand
objective:
sense: minimize
description: what the input flows cost
expression: sum(sum(rate * fuel_price, over=flow), over=time)
The model-building half of examples/ports/references/linopy/piecewise_conversion.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``.
"""
flows: pd.DataFrame = tables['flow'].set_index('flow')
times = pd.Index(tables['time']['time'], name='time')
present = tables['bp_present'].set_index(['converter', 'bp'])['value']
runs_to = {c: int(present[c].sum()) for c in present.index.get_level_values('converter').unique()}
m = linopy.Model()
rate = m.add_variables(lower=0, coords=[pd.Index(flows.index, name='flow'), times], name='rate')
for converter, members in flows.groupby('converter_of'):
pairs = [
(rate.sel(flow=flow, drop=True), linopy.breakpoints(curve_of(tables, flow, runs_to[converter])))
for flow in members.index
]
m.add_piecewise_formulation(*pairs, name=f'curve_{converter}')
for carrier, demand in (('is_heat', 'heat_demand'), ('is_power', 'power_demand')):
weights = tables[carrier].set_index('flow')['value'].reindex(flows.index)
wanted = tables[demand].set_index('time')['value'].reindex(times)
m.add_constraints((rate * weights).sum('flow') == wanted, name=demand)
price = tables['fuel_price'].set_index('flow')['value'].reindex(flows.index)
m.add_objective((rate * price).sum())
return m
What it exercises¶
The arity is a row count. on_the_curve builds one row per flow, so a
converter tying three expressions and one tying two are the same declaration.
Nothing in the file says how many flows a converter has; the converter_of
relation does, and it is data.
The mask is the variable's own. where: bp_present decides which weights
exist, and an sos: set is over the
members present, so the boiler's three-breakpoint curve and the CHP's four sit
on one axis with nothing padded. A solver without SOS gets binaries and linking
rows for the same set, which multiply by the member's own upper bound — 1 here,
because a weight is at most one.
linopy's add_piecewise_formulation ties N expressions to one basis too, but
its pairs are an argument list, so the linopy tab writes the arity out per
converter in a Python loop. That loop is what moves into the data here.
examples/piecewise_conversion.yaml · back to all models