mathspec#
Write the specification (spec) of an optimisation model as a YAML file. Check it and print it as math, with no data and no solver.
What it is for#
- Check specs in CI, with no data. A wrong name or dimension fails when the file loads, and the error names the fix. Errors →
- Publish the math you solve. The equations in the paper print from the file the solver reads. Typeset →
- One spec, many tools. Engines, renderers and analysers read the spec through one public API, so no two of them can read the file differently. Program API →
- Write full-size specs. The spec of PyPSA's
n.optimize()model is one file, with stochastic, multi-period and quadratic variants. PyPSA in one file →
mathspec builds nothing and solves nothing itself. specsolve and linopy build a model from a spec and its data, and solve it. Support in both is work in progress.
A spec is one file#
A file states one specification, or spec. A spec declares four things: the axes it runs over, the data it expects, the decisions the solver makes, and the rules those decisions obey. It holds no data: an engine attaches the data and builds a model. The file below is a complete spec.
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 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#
Subject to#
power_balance
Variable domains#
dispatch
\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 mathspec 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')
ms.to_latex(spec, symbols=symbols)
ms.to_typst(spec)
ms.to_markdown(spec)
symbols gives every name its conventional spelling. Pass a dict, a YAML path
or a SymbolTable. It is optional: drop it and the same spec 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 mathspec latex dispatch.yaml --symbols dispatch.symbols.yaml
python -m mathspec 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#
- Your first spec: write the file above one block at a time, check it and print it.
- The language: what a file may contain, and what it means.
- Examples: whole specs, each beside the math it prints.
- Print a spec as math: LaTeX, Typst or Markdown, from the file alone.
- Check a spec without data: on your machine and in CI.
- Reading a spec and its program: for whoever writes an engine or a renderer.
Install it#
See installation.
Alpha, pre-1.0
Breaking changes land without a deprecation cycle. Pin an exact version if you depend on this, and read the changelog before upgrading. Every construct round-trips through the schema, the parsers and all three typeset formats, and the LaTeX is compiled. The accepted YAML is not yet frozen.