Skip to content

Your first spec#

In this tutorial you build the specification of an optimisation model (a spec) for least-cost dispatch. In this model a fleet of generators meets an hourly load at the lowest cost. You build the file one section at a time, check it after each step, and print it as math at the end. This tutorial takes about 15 minutes. Before you start, install mathspec.

One YAML file states the spec. The file contains no data and needs no solver, so you run every step on the file alone.

Dimensions#

A dimension is a set the spec is indexed over, such as the hours of a year or the generators of a fleet. Make a file dispatch.yaml with a description and two dimensions:

dispatch.yaml
description: Least-cost dispatch of a generator fleet against an hourly load.

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

Check the file:

python -m mathspec check dispatch.yaml
import mathspec as ms

for note in ms.advice('dispatch.yaml'):
    print(note)
dimension 'generator' is never used: nothing is indexed by it, nothing sums into it, and no relation has a column over it. Remove it, or keep it if the entries that use it are still to be written.
dimension 'snapshot' is never used: nothing is indexed by it, nothing sums into it, and no relation has a column over it. Remove it, or keep it if the entries that use it are still to be written.

mathspec accepts the file. check prints one remark that is not a refusal (a line of advice) for each dimension. No named parameter, variable or constraint (an entry) in the file uses either dimension yet.

Keep this Python session open. Later steps use ms.

Parameters and a variable#

A parameter is data the spec requires, and a variable is a decision the solver makes. Add three parameters and one variable to the file:

parameters:
  capacity: { dims: [generator], description: installed capacity }
  load: { dims: [snapshot], description: demand to be met }
  cost: { dims: [generator], description: marginal cost }

variables:
  dispatch:
    description: output of a generator in a snapshot
    dims: [snapshot, generator]
    where: "capacity > 0"
    bounds: { lower: 0, upper: capacity }

The dims: key lists the dimensions an entry is indexed over. With data attached, capacity has one value for each generator, and dispatch has one for each dispatch period (snapshot) and generator.

The where: line is a condition. With data attached, a generator with no capacity has no dispatch (it is absent).

Print the math with markdown or to_markdown. The --no-legend flag and legend=False leave out the tables of symbols:

python -m mathspec markdown --no-legend dispatch.yaml
print(ms.to_markdown('dispatch.yaml', legend=False))

Rendered output

Least-cost dispatch of a generator fleet against an hourly load.

Variable domains#

dispatch

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

In the equation, dispatch lies between zero and the capacity for every snapshot \(t\) and every generator \(g\) whose capacity is more than zero.

A constraint and the objective#

A constraint is a rule the decisions obey, and the objective is the quantity the solver makes as small or as large as it can. Add a constraint that meets the load in every snapshot, and an objective that minimises the cost:

constraints:
  power_balance:
    dims: [snapshot]
    expression: sum(dispatch, over=generator) == load

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

The sum adds the dispatch of every generator, so with data attached, power_balance has one row for each snapshot. Check the file again:

python -m mathspec check dispatch.yaml
for note in ms.advice('dispatch.yaml'):
    print(note)

check prints nothing, because every dimension is now used.

An undeclared name#

When mathspec first reads a file (at load), it refuses the file if an expression uses a name the file does not declare. Change cost to costs in the objective:

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

Check the file:

python -m mathspec check dispatch.yaml
for note in ms.advice('dispatch.yaml'):
    print(note)
The objective: 'costs' not found.
  Variables: ['dispatch']
  Parameters: ['capacity', 'cost', 'load']
Check the spelling, or declare 'costs'.

mathspec refuses the file. The command exits with status 1, and advice raises SchemaError. mathspec names the entry with the unknown name, lists the names the file declares, and suggests what to write instead.

Change costs back to cost:

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

Check the file again:

python -m mathspec check dispatch.yaml
for note in ms.advice('dispatch.yaml'):
    print(note)

check prints nothing again.

The math#

The whole file is now:

dispatch.yaml
description: Least-cost dispatch of a generator fleet against an hourly load.

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

parameters:
  capacity: { dims: [generator], description: installed capacity }
  load: { dims: [snapshot], description: demand to be met }
  cost: { dims: [generator], description: marginal cost }

variables:
  dispatch:
    description: output of a generator in a snapshot
    dims: [snapshot, generator]
    where: "capacity > 0"
    bounds: { lower: 0, upper: capacity }

constraints:
  power_balance:
    dims: [snapshot]
    expression: sum(dispatch, over=generator) == load

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

The file contains every entry the steps above added. Print the whole spec:

python -m mathspec markdown dispatch.yaml
print(ms.to_markdown('dispatch.yaml'))

Rendered output

Least-cost dispatch of a generator fleet against an hourly load.

Sets#

Symbol Meaning
\(\mathcal{G}\) index \(g\) — generator — generating units
\(\mathcal{T}\) index \(t\) — snapshot — dispatch periods

Parameters#

Symbol Meaning
\(\mathrm{capacity}\) capacity over \(\mathcal{G}\) — installed capacity
\(\mathrm{cost}\) cost over \(\mathcal{G}\) — marginal cost
\(\mathrm{load}\) load over \(\mathcal{T}\) — demand to be met

Variables#

Symbol Meaning
\(\mathit{dispatch}\) dispatch over \(\mathcal{G} \times \mathcal{T}\) — output of a generator in a snapshot

Variable domains#

dispatch

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

Objective#

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

Subject to#

power_balance

\[ \sum_{g \in \mathcal{G}} \mathit{dispatch}_{g,t} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T} \]

The tables at the top list the symbol and description of each set, parameter and variable. Below them, the bounds of dispatch, the objective and power_balance each appear as one equation.

What you built#

You have built a least-cost dispatch spec in one file. It declares two dimensions, three parameters, a variable with bounds, a constraint and an objective. check accepts it, and markdown prints it as math.

Where to next#

See also