A spec in several files#
In this lesson you build a dispatch spec out of files that each hold one part of it, merge them into one spec, and then add a component without changing the other files. Do your first spec first.
The network#
Make a file network.yaml. It balances every bus, and it reads the injection
at a bus under given:: what the
components put in is theirs to say, in
a term each.
description: Every bus is balanced in every snapshot.
dimensions:
snapshot: { dtype: int, description: dispatch periods }
bus: { description: network nodes }
given:
expressions:
injection:
dims: [snapshot, bus]
description: what the components put into a bus, less what they take out
constraints:
balance:
dims: [snapshot, bus]
expression: injection == 0
Check the file:
The check accepts it, and notes the name it reads and does not define:
expression 'injection' is read here and declared elsewhere: the model this one is layered onto provides it. A consumer checks that it does, on the same frame, and refuses the program where it does not. A fragment is composed instead: merge() folds this declaration into the one a sibling introduces, or writes it from the terms siblings add.
The generators#
Make a file generators.yaml. It says what the fleet puts into a bus as a
named expression, generation. It reads the injection too, and
adds_to: on generation names
the injection as what the expression adds to. The two dimensions it shares
with the network it restates as a dtype and nothing else: a description is not
a claim, and merge carries the network's.
description: A generator fleet, each unit on one bus.
dimensions:
snapshot: { dtype: int }
bus: { dtype: str }
generator: { description: generating units }
relations:
gen_bus: { key: generator, values: bus, description: the bus a generator sits on }
parameters:
capacity: { dims: [generator], description: installed capacity }
cost: { dims: [generator], description: marginal cost }
variables:
dispatch:
description: output of a generator in a snapshot
dims: [snapshot, generator]
bounds: { lower: 0, upper: capacity }
given:
expressions:
injection: { dims: [snapshot, bus] }
expressions:
generation:
description: what the fleet puts into a bus
expression: sum(dispatch, by=gen_bus, over=generator, into=bus)
adds_to: injection
objective:
sense: minimize
expression: sum(dispatch * cost)
Check the file:
The check accepts it, and notes the term:
expression 'injection' is read here, and this file adds a term to it: merge() sums the term with what the other files write under the name. Until then, the program reads it and does not build it.
Print the math of the file on its own:
The term prints as its own definition, and the legend, left out here, says what it adds to:
Rendered output
A generator fleet, each unit on one bus.
Objective#
Definitions#
generation
Variable domains#
dispatch
The loads#
Make a file loads.yaml. Its term, consumption, takes the demand out of the
bus:
description: The demand at every bus.
dimensions:
snapshot: { dtype: int }
bus: { dtype: str }
parameters:
demand: { dims: [snapshot, bus], description: demand to be met }
given:
expressions:
injection: { dims: [snapshot, bus] }
expressions:
consumption:
description: what the loads take out of a bus
expression: -demand
adds_to: injection
Check the file:
The check accepts it, with the same note:
expression 'injection' is read here, and this file adds a term to it: merge() sums the term with what the other files write under the name. Until then, the program reads it and does not build it.
Merge the files#
Merge the three files in Python. Give them as a list:
spec = ms.merge(['network.yaml', 'generators.yaml', 'loads.yaml'])
print(spec.expressions['injection'].expression)
print(spec.dimensions['snapshot'].description)
The injection is the sum of the two terms by name, in the order of the list. Each term stays a named expression of the merged spec. The dimension carries the one description written for it, the network's:
Print the math of the merged spec:
Rendered output
Objective#
Subject to#
balance
Definitions#
generation
consumption
injection
Variable domains#
dispatch
Add a component#
Make a file imports.yaml. It adds a term, purchase, to the injection and a
term to the objective:
description: Power bought from outside the network, at a price.
dimensions:
snapshot: { dtype: int }
bus: { dtype: str }
parameters:
import_limit: { dims: [bus], description: most a bus can import }
import_price: { dims: [snapshot], description: price of imported power }
variables:
imported:
description: power a bus imports in a snapshot
dims: [snapshot, bus]
bounds: { lower: 0, upper: import_limit }
given:
expressions:
injection: { dims: [snapshot, bus] }
expressions:
purchase:
description: what the imports put into a bus
expression: imported
adds_to: injection
objective:
sense: minimize
expression: sum(imported * import_price)
Merge the four files:
spec = ms.merge(['network.yaml', 'generators.yaml', 'loads.yaml', 'imports.yaml'])
print(spec.expressions['injection'].expression)
print(spec.objective.expression)
The injection has a third term at the end, and the objective sums the two
objectives. network.yaml did not change:
Read what another file declares#
Make a file emissions.yaml. It caps what the fleet emits, and it reads
dispatch under given: rather than declaring it:
description: A cap on what the fleet emits over the horizon.
dimensions:
snapshot: { dtype: int }
generator: { dtype: str }
given:
variables:
dispatch: { dims: [snapshot, generator] }
parameters:
emission_rate: { dims: [generator], description: emissions per unit of output }
emission_cap: { dims: [], description: most the fleet may emit }
constraints:
emission_limit:
dims: []
expression: sum(dispatch * emission_rate) <= emission_cap
Check the file:
The check accepts it, and notes the variable it reads:
variable 'dispatch' is read here and declared elsewhere: the model this one is layered onto provides it. A consumer checks that it does, on the same frame, and refuses the program where it does not. A fragment is composed instead: merge() folds this declaration into the one a sibling introduces.
Merge all five files:
spec = ms.merge(['network.yaml', 'generators.yaml', 'loads.yaml', 'imports.yaml', 'emissions.yaml'])
print(sorted(spec.constraints))
print(bool(spec.program.given))
The cap reads the generators' dispatch, and nothing is left for anything
outside the files to provide:
Leave the network out#
Merge the generators and the loads without the network:
merge refuses it. A term adds to a name another file declares, and without
the network no file declares injection:
fragments 'generators.yaml' and 'loads.yaml' add a term to 'injection', and no other fragment reads it: none reads it without adding to it, or uses it in its math. A term writes into a sum the rest of the spec reads: add the fragment that reads it, or fix the spelling under 'given:'.
Where to next#
- Compose a spec from several files covers
mergeandoverride, which lays a patch over a spec. givengives every rule a file that reads another file obeys.- A component library shows larger fragments beside the math they print.