Skip to content

math-spec#

Write an optimisation model as a YAML file. Check it and print it as math, with no data and no solver.

CI conda-forge pypi-version python-version Documentation build status

Read the language See the examples


A model is one file#

A file declares four things: the axes the model runs over, the data it expects, the decisions the solver makes, and the rules those decisions obey. The file below is a complete model.

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)

Everything that can be checked without data is checked when the file loads. A misspelled name, a where: on an undeclared parameter, or a constraint whose dimensions do not match its dims: is refused with a message that names the fix.

The math it prints#

Printed from the file above, with no data and no solver. How shows the call.

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

Sets#

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

Parameters#

Symbol Meaning
\(\bar p\) capacity over \(\mathcal{G}\) — installed capacity
\(\ell\) load over \(\mathcal{S}\) — demand to be met
\(c\) cost over \(\mathcal{G}\) — marginal cost

Variables#

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

Objective#

\[ \min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g} \]

Subject to#

power_balance

\[ \sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S} \]

Variable domains#

dispatch

\[ 0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0 \]
\noindent Least-cost dispatch of a generator fleet against an hourly load.

\paragraph{Sets}
\begin{description}
\item[{$\mathcal{S}$}] index $s$ --- \texttt{snapshot} --- dispatch periods
\item[{$\mathcal{G}$}] index $g$ --- \texttt{generator} --- generating units
\end{description}

\paragraph{Parameters}
\begin{description}
\item[{$\bar p$}] \texttt{capacity} over $\mathcal{G}$ --- installed capacity
\item[{$\ell$}] \texttt{load} over $\mathcal{S}$ --- demand to be met
\item[{$c$}] \texttt{cost} over $\mathcal{G}$ --- marginal cost
\end{description}

\paragraph{Variables}
\begin{description}
\item[{$\mathit{dispatch}$}] \texttt{dispatch} over $\mathcal{S} \times \mathcal{G}$ --- output of a generator in a snapshot
\end{description}

\paragraph{Objective}
\begin{align*}
 && \min & \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}
\end{align*}

\paragraph{Subject to}
\begin{align*}
\text{power\_balance} && \sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} & = \ell_{s} && \forall\, s \in \mathcal{S}
\end{align*}

\paragraph{Variable domains}
\begin{align*}
\text{dispatch} && 0 \le \mathit{dispatch}_{s,g} & \le \bar p_{g} && \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
\end{align*}
import math_spec as ms

symbols = {
    'notation': 'latex',
    'dimensions': {
        'snapshot': {'index': 's', 'set': '\\mathcal{S}'},
        'generator': {'index': 'g', 'set': '\\mathcal{G}'},
    },
    'names': {
        'cost': 'c',
        'load': '\\ell',
        'capacity': '\\bar p',
    },
}

spec = ms.to_spec('dispatch.yaml')  # read and checked once, then printed three ways

ms.to_latex(spec, symbols=symbols)  # amsmath align
ms.to_typst(spec)  # compiles without a TeX toolchain
ms.to_markdown(spec)  # renders as-is on GitHub

symbols gives every name its conventional spelling. Pass a dict, a YAML path or a SymbolTable. It is optional: drop it and the same model prints from the names in the file, as \(\mathrm{load}_t\) and \(\mathrm{capacity}_g\).

Or from a shell, where the table is that same YAML on disk. --standalone emits a document that compiles, rather than a fragment to \input:

python -m math_spec latex dispatch.yaml --symbols dispatch.symbols.yaml
python -m math_spec typst dispatch.yaml --standalone -o dispatch.typ

Typeset the math documents the three functions, their options and symbol tables. Each reads the same file every other page here loads.

Where to next#

Install it#

git clone https://github.com/energy-models/math-spec
cd math-spec

pixi run pre-commit-install
pixi run test

Or as a dependency, once the project leaves the alpha stream. See installation for every package manager.

Alpha, pre-1.0

Breaking changes land without a deprecation cycle. When a construct is named wrong, a default is wrong, or a permissive input hides a silent wrong answer, it is fixed rather than aliased. A compatibility shim for every earlier spelling would defeat the point of a small language.

Pin an exact version if you depend on this, and read the changelog before upgrading. What exists is tested: every construct the language has round-trips through the schema, the parsers and all three typeset formats, and the LaTeX is compiled rather than eyeballed. It is the accepted YAML that is not yet frozen, not the behaviour.