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. It also sets the
objective, and the objective reads the total cost the same way: what each
component costs is a term of its own.
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
total_cost:
dims: []
description: what running the system costs
constraints:
balance:
dims: [snapshot, bus]
expression: injection == 0
objective:
sense: minimize
expression: total_cost
Check the file:
The check accepts it, and notes each 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.
expression 'total_cost' 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. What the fleet costs is a term
of the total cost in the same way, generation_cost. 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] }
total_cost: { dims: [] }
expressions:
generation:
description: what the fleet puts into a bus
expression: sum(dispatch, by=gen_bus, over=generator, into=bus)
adds_to: injection
generation_cost:
description: what running the fleet costs
expression: sum(dispatch * cost)
adds_to: total_cost
Check the file:
The check accepts it, and notes each 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.
expression 'total_cost' 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:
Each term prints as its own definition, and the legend, left out here, says what it adds to. The file sets no objective, so on its own it is a feasibility problem:
Rendered output
A generator fleet, each unit on one bus.
Definitions#
generation
generation_cost
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
generation_cost
consumption
injection
total_cost
Variable domains#
dispatch
Add a component#
Make a file imports.yaml. It adds a term, purchase, to the injection and a
term, import_cost, to the total cost:
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] }
total_cost: { dims: [] }
expressions:
purchase:
description: what the imports put into a bus
expression: imported
adds_to: injection
import_cost:
description: what the imports cost
expression: sum(imported * import_price)
adds_to: total_cost
Merge the four files:
spec = ms.merge(['network.yaml', 'generators.yaml', 'loads.yaml', 'imports.yaml'])
print(spec.expressions['injection'].expression)
print(spec.expressions['total_cost'].expression)
The injection has a third term at the end, and the total cost a second one.
network.yaml did not change:
Only network.yaml sets the objective. A second file that sets one is
refused: merge does not join two objectives, and a part of the cost is a
term of total_cost.
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 sum that the rest of the spec reads, and
without the network no file reads injection other than the two files that
add to it:
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.