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.
And the same library, operated rather than planned: operate.yaml is a
patch laid over the
composition, and the whole of it is that capacity stops being a decision.
Every constraint expression and the objective are identical either side of it —
the cost of capacity becoming the sunk constant it is.
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_invest: { dims: [generator] }
gen_cap_max: { dims: [generator] }
variables:
gen_cap:
description: capacity built, which an operating model is told rather than decides
foreach: [generator]
bounds: { lower: 0, upper: gen_cap_max }
gen_p:
foreach: [snapshot, generator]
bounds: { lower: 0 }
constraints:
gen_injects:
foreach: [snapshot, generator]
expression: at(flow, by=gen_port) == gen_p
gen_within_capacity:
foreach: [snapshot, generator]
expression: gen_p <= gen_cap
objective:
sense: minimize
expression: sum(gen_cap * gen_invest) + 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\_invest}\) | gen_invest over \(\mathcal{G}\) |
| \(\mathrm{gen\_cap\_max}\) | gen_cap_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\_cap}\) | gen_cap over \(\mathcal{G}\) — capacity built, which an operating model is told rather than decides |
| \(\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
gen_within_capacity
dem_withdraws
st_injects
st_soc_balance
Variable domains#
flow
gen_cap
gen_p
st_charge
st_discharge
st_soc
examples/composed/operate.yaml#
description: >-
The same system, operating a fleet somebody already built. The whole patch is
that capacity stops being a decision: the constraint that reads it is
untouched, and its cost becomes the sunk constant it is.
variables:
gen_cap: null
parameters:
gen_cap: { dims: [generator] }
The model that patch makes#
The same system, operating a fleet somebody already built. The whole patch is that capacity stops being a decision: the constraint that reads it is untouched, and its cost becomes the sunk constant it is.
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\_invest}\) | gen_invest over \(\mathcal{G}\) |
| \(\mathrm{gen\_cap\_max}\) | gen_cap_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) |
| \(\mathrm{gen\_cap}\) | gen_cap over \(\mathcal{G}\) |
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
gen_within_capacity
dem_withdraws
st_injects
st_soc_balance
Variable domains#
flow
gen_p
st_charge
st_discharge
st_soc