Skip to content

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. It defines the injection at a bus as an empty expression, expression: null, which the other files fill:

network.yaml
description: Every bus is balanced in every snapshot.

dimensions:
  snapshot: { dtype: int, description: dispatch periods }
  bus: { description: network nodes }

expressions:
  injection:
    dims: [snapshot, bus]
    expression: null
    description: what the components put into a bus, less what they take out

constraints:
  balance:
    dims: [snapshot, bus]
    expression: injection == 0

Check the file:

python -m mathspec check network.yaml

The check accepts it, and prints nothing.

Print the math of the file on its own:

import mathspec as ms

print(ms.to_markdown('network.yaml', legend=False))

The file does not know what the injection holds, so its definition prints as dots:

Rendered output

Every bus is balanced in every snapshot.

Subject to#

balance

\[ \mathit{injection}_{t,b} = 0 \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

Definitions#

injection

\[ \mathit{injection}_{t,b} = \dots \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

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 a term: on the entry names generation as what this file adds to it.

generators.yaml
description: A generator fleet, each unit on one bus.

dimensions:
  snapshot: { dtype: int, description: dispatch periods }
  bus: { description: network nodes }
  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 }

expressions:
  generation:
    description: what the fleet puts into a bus
    expression: sum(dispatch, by=gen_bus, over=generator, into=bus)

given:
  expressions:
    injection:
      dims: [snapshot, bus]
      term: generation

objective:
  sense: minimize
  expression: sum(dispatch * cost)

Check the file:

python -m mathspec check generators.yaml

The check accepts it, and notes the term:

expression 'injection' is read here and declared elsewhere, and this file adds a term to it: merge() sums the term with what the other files declare under the name. Until then, the program reads it and does not build it.

Print the math of the file on its own:

print(ms.to_markdown('generators.yaml', legend=False))

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#

\[ \min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{dispatch}_{t,g} \cdot \mathrm{cost}_{g} \]

Definitions#

generation

\[ \mathit{generation}_{t,b} = \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} \mathit{dispatch}_{t,g} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

Variable domains#

dispatch

\[ 0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \]

The loads#

Make a file loads.yaml. Its term, consumption, takes the demand out of the bus:

loads.yaml
description: The demand at every bus.

dimensions:
  snapshot: { dtype: int, description: dispatch periods }
  bus: { description: network nodes }

parameters:
  demand: { dims: [snapshot, bus], description: demand to be met }

expressions:
  consumption:
    description: what the loads take out of a bus
    expression: -demand

given:
  expressions:
    injection:
      dims: [snapshot, bus]
      term: consumption

Check the file:

python -m mathspec check loads.yaml

The check accepts it, with the same note:

expression 'injection' is read here and declared elsewhere, and this file adds a term to it: merge() sums the term with what the other files declare under the name. Until then, the program reads it and does not build it.

Merge the files#

Merge the three files in Python. Each name is what an error calls that file:

spec = ms.merge({'network': 'network.yaml', 'generators': 'generators.yaml', 'loads': 'loads.yaml'})
print(spec.expressions['injection'].expression)

The injection is the sum of the two terms by name, in the order of the file names, and the empty definition adds nothing. Each term stays a named expression of the merged spec:

generation + consumption

Print the math of the merged spec:

print(ms.to_markdown(spec, legend=False))

Rendered output

Objective#

\[ \min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{dispatch}_{t,g} \cdot \mathrm{cost}_{g} \]

Subject to#

balance

\[ \mathit{injection}_{t,b} = 0 \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

Definitions#

injection

\[ \mathit{injection}_{t,b} = \mathit{generation}_{t,b} + \mathrm{consumption}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

generation

\[ \mathit{generation}_{t,b} = \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} \mathit{dispatch}_{t,g} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

consumption

\[ \mathrm{consumption}_{t,b} = -\mathrm{demand}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \]

Variable domains#

dispatch

\[ 0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \]

Add a component#

Make a file imports.yaml. It adds a term, purchase, to the injection and a term to the objective:

imports.yaml
description: Power bought from outside the network, at a price.

dimensions:
  snapshot: { dtype: int, description: dispatch periods }
  bus: { description: network nodes }

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 }

expressions:
  purchase:
    description: what the imports put into a bus
    expression: imported

given:
  expressions:
    injection:
      dims: [snapshot, bus]
      term: purchase

objective:
  sense: minimize
  expression: sum(imported * import_price)

Merge the four files:

spec = ms.merge(
    {'network': 'network.yaml', 'generators': 'generators.yaml', 'loads': 'loads.yaml', 'imports': 'imports.yaml'}
)
print(spec.expressions['injection'].expression)
print(spec.objective.expression)

The injection has a third term, and the objective sums the two objectives. network.yaml did not change:

generation + purchase + consumption
(sum(dispatch * cost)) + (sum(imported * import_price))

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:

emissions.yaml
description: A cap on what the fleet emits over the horizon.

dimensions:
  snapshot: { dtype: int, description: dispatch periods }
  generator: { description: generating units }

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:

python -m mathspec check emissions.yaml

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': 'network.yaml',
        'generators': 'generators.yaml',
        'loads': 'loads.yaml',
        'imports': 'imports.yaml',
        'emissions': '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:

['balance', 'emission_limit']
False

Leave the network out#

Merge the generators and the loads without the network:

ms.merge({'generators': 'generators.yaml', 'loads': 'loads.yaml'})

merge refuses it. A term adds to a definition another file writes, and without the network no file defines injection:

fragments 'generators' and 'loads' add a term to 'injection', which no fragment defines. A term adds to a definition another file writes under 'expressions:': define it there, as `expression: null` where the terms are all of it, or fix the spelling.

Where to next#