A system composed from templates#
Four files, none of them a model. base.yaml declares the coupling surface —
one flow per port, and one balance per bus — and the other three name flow
without declaring it, which is why each is a load error on its own and the
composition is not.
import math_spec as ms
model = ms.merge(
{
'base': 'examples/composed/base.yaml',
'generator': 'examples/composed/generator.yaml',
'demand': 'examples/composed/demand.yaml',
'storage': 'examples/composed/storage.yaml',
},
description='A power system composed from four templates',
)
The balance does not grow when a component type is added. A fourth
component adds its own declarations and its own cost, and
sum(flow, by=port_bus) == 0 is the same row it was with three — which is the
whole reason each component pins its port's flow with
at rather than owning a flow variable of
its own. Drop storage.yaml from the call above and every other line of the
composed model is unchanged.
Topology is data. No file here says which bus a generator sits on: gen_port
and port_bus are lookups, and wiring a
specific system is rows in those two tables. Structure is bounded by the four
component types; how many generators there are is a question only the data
answers.
One name is one declaration. Merging refuses a name two fragments both
declare, so the templates here are spelled apart — gen_p, st_soc,
dem_load. If two of these were the same kind of thing with different numbers,
they would be two rows in one dimension rather than two fragments.
examples/composed/base.yaml#
description: >-
The coupling surface every component agrees on: one flow per port, and one
balance per bus. Every other fragment names `flow` and declares none of it.
dimensions:
snapshot: { dtype: int }
bus: { dtype: str }
port: { dtype: str }
lookups:
port_bus: { over: port, into: bus }
variables:
flow:
description: what a port puts into its bus in a snapshot, negative for a withdrawal
foreach: [snapshot, port]
constraints:
balance:
description: every bus clears
foreach: [snapshot, bus]
expression: sum(flow, by=port_bus) == 0
examples/composed/generator.yaml#
description: A fleet of generators, each on one port.
dimensions:
snapshot: { dtype: int }
port: { dtype: str }
generator: { dtype: str }
lookups:
gen_port: { over: generator, into: port }
parameters:
gen_cost: { dims: [generator] }
gen_p_max: { dims: [generator] }
variables:
gen_p:
foreach: [snapshot, generator]
bounds: { lower: 0, upper: gen_p_max }
constraints:
gen_injects:
foreach: [snapshot, generator]
expression: at(flow, by=gen_port) == gen_p
objective:
sense: minimize
expression: sum(gen_p * gen_cost)
examples/composed/demand.yaml#
description: Fixed demands, each on one port.
dimensions:
snapshot: { dtype: int }
port: { dtype: str }
demand: { dtype: str }
lookups:
dem_port: { over: demand, into: port }
parameters:
dem_load: { dims: [snapshot, demand] }
constraints:
dem_withdraws:
foreach: [snapshot, demand]
expression: at(flow, by=dem_port) == -dem_load
examples/composed/storage.yaml#
description: Stores, each on one port, charging and discharging against a state of charge.
dimensions:
snapshot: { dtype: int }
port: { dtype: str }
store: { dtype: str }
lookups:
st_port: { over: store, into: port }
parameters:
st_capacity: { dims: [store] }
st_holding: { dims: [] }
variables:
st_charge: { foreach: [snapshot, store], bounds: { lower: 0 } }
st_discharge: { foreach: [snapshot, store], bounds: { lower: 0 } }
st_soc: { foreach: [snapshot, store], bounds: { lower: 0, upper: st_capacity } }
constraints:
st_injects:
foreach: [snapshot, store]
expression: at(flow, by=st_port) == st_discharge - st_charge
st_soc_balance:
foreach: [snapshot, store]
expression: st_soc == shift(st_soc, over=snapshot, offset=1, edge=0) + st_charge - st_discharge
objective:
sense: minimize
expression: sum(st_soc) * st_holding
The one model they make#
A power system composed from four templates
Sets#
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) — snapshot |
| \(\mathcal{B}\) | index \(b\) — bus |
| \(\mathcal{P}\) | index \(p\) — port with \(\mathrm{port\_bus}: \mathcal{P} \to \mathcal{B}\) |
| \(\mathcal{G}\) | index \(g\) — generator with \(\mathrm{gen\_port}: \mathcal{G} \to \mathcal{P}\) |
| \(\mathcal{D}\) | index \(d\) — demand with \(\mathrm{dem\_port}: \mathcal{D} \to \mathcal{P}\) |
| \(\mathcal{S}\) | index \(s\) — store with \(\mathrm{st\_port}: \mathcal{S} \to \mathcal{P}\) |
Parameters#
| Symbol | Meaning |
|---|---|
| \(\mathrm{gen\_cost}\) | gen_cost over \(\mathcal{G}\) |
| \(\mathrm{gen\_p\_max}\) | gen_p_max over \(\mathcal{G}\) |
| \(\mathrm{dem\_load}\) | dem_load over \(\mathcal{T} \times \mathcal{D}\) |
| \(\mathrm{st\_capacity}\) | st_capacity over \(\mathcal{S}\) |
| \(\mathrm{st\_holding}\) | st_holding (scalar) |
Variables#
| Symbol | Meaning |
|---|---|
| \(\mathit{flow}\) | flow over \(\mathcal{T} \times \mathcal{P}\) — what a port puts into its bus in a snapshot, negative for a withdrawal |
| \(\mathit{gen\_p}\) | gen_p over \(\mathcal{T} \times \mathcal{G}\) |
| \(\mathit{st\_charge}\) | st_charge over \(\mathcal{T} \times \mathcal{S}\) |
| \(\mathit{st\_discharge}\) | st_discharge over \(\mathcal{T} \times \mathcal{S}\) |
| \(\mathit{st\_soc}\) | st_soc over \(\mathcal{T} \times \mathcal{S}\) |
Upright is what the model is given — a parameter such as \(\mathrm{gen\_cost}\), a coordinate map, a label — and italic is what the solver chooses, such as \(\mathit{flow}\). 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
gen_injects
dem_withdraws
st_injects
st_soc_balance
Variable domains#
flow
gen_p
st_charge
st_discharge
st_soc