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 a spec with no data. mathspec refuses a misspelt name or a dimension mismatch at load, so a spec is checked in continuous integration (CI) like code. Errors →
- Publish the math you solve. One file states the spec. An engine builds the model from it, and mathspec prints its equations for a paper. Typeset →
- One spec, many tools. Engines, renderers (tools that print a spec) and checkers read a spec through one interface, so no two read a file differently. Program API →
- Write full-size specs. The model that
n.optimize()builds in the power system framework PyPSA is one spec file, with its stochastic, multi-period and quadratic classes. PyPSA in one file →
The spec#
A spec declares four things:
- the dimensions it is indexed over, such as
snapshotandgenerator - the parameters it takes as data, such as
loadandcost - the decisions a solver makes, such as
dispatch - the rules those decisions obey, such as
sum(dispatch, over=generator) == load
A spec contains no data. A tool that builds the model (an engine) attaches the data to the spec. The file below is a complete spec.
description: Least-cost dispatch of a generator fleet against a load in each dispatch period.
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#
mathspec prints the math below from the file above. The call that prints it is in the How tab.
Least-cost dispatch of a generator fleet against a load in each dispatch period.
Sets#
| Symbol | Meaning |
|---|---|
| \(\mathcal{G}\) | index \(g\) — generator — generating units |
| \(\mathcal{S}\) | index \(s\) — snapshot — dispatch periods |
Parameters#
| Symbol | Meaning |
|---|---|
| \(\bar p\) | capacity over \(\mathcal{G}\) — installed capacity |
| \(c\) | cost over \(\mathcal{G}\) — marginal cost |
| \(\ell\) | load over \(\mathcal{S}\) — demand to be met |
Variables#
| Symbol | Meaning |
|---|---|
| \(\mathit{dispatch}\) | dispatch over \(\mathcal{G} \times \mathcal{S}\) — output of a generator in a snapshot |
Variable domains#
dispatch
Objective#
Subject to#
power_balance
\noindent Least-cost dispatch of a generator fleet against a load in each dispatch period.
\paragraph{Sets}
\begin{description}
\item[{$\mathcal{G}$}] index $g$ --- \texttt{generator} --- generating units
\item[{$\mathcal{S}$}] index $s$ --- \texttt{snapshot} --- dispatch periods
\end{description}
\paragraph{Parameters}
\begin{description}
\item[{$\bar p$}] \texttt{capacity} over $\mathcal{G}$ --- installed capacity
\item[{$c$}] \texttt{cost} over $\mathcal{G}$ --- marginal cost
\item[{$\ell$}] \texttt{load} over $\mathcal{S}$ --- demand to be met
\end{description}
\paragraph{Variables}
\begin{description}
\item[{$\mathit{dispatch}$}] \texttt{dispatch} over $\mathcal{G} \times \mathcal{S}$ --- output of a generator in a snapshot
\end{description}
\paragraph{Variable domains}
\begin{align*}
\text{dispatch} && 0 \le \mathit{dispatch}_{g,s} & \le \bar p_{g} && \forall\, g \in \mathcal{G},\ s \in \mathcal{S} \,:\, \bar p_{g} > 0
\end{align*}
\paragraph{Objective}
\begin{align*}
&& \min & \sum_{g \in \mathcal{G},\ s \in \mathcal{S}} \mathit{dispatch}_{g,s} \cdot c_{g}
\end{align*}
\paragraph{Subject to}
\begin{align*}
\text{power\_balance} && \sum_{g \in \mathcal{G}} \mathit{dispatch}_{g,s} & = \ell_{s} && \forall\, s \in \mathcal{S}
\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 sets the symbol for each name. 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 output documents the three functions, their options and symbol tables. Each reads the same file every other page here loads.
Engines and other tools#
An engine, such as specsolve, attaches data to a spec, builds the model and hands it to a solver. mathspec itself builds no model. Any other tool, such as a renderer or a checker, reads the same spec through the Program API. In the diagram, the solid boxes are mathspec and the dashed boxes are outside it.
flowchart LR
accTitle: What mathspec does, and what other tools do with a spec
accDescr: A YAML file loads into a Spec and the Program it compiles to. mathspec checks the spec and prints it as math, with no data. Outside mathspec, drawn dashed, an engine such as specsolve reads the same spec, takes your data and returns your answers, and any other tool, such as a renderer or a checker, reads the same spec through the Program API.
Y(["your spec<br/>one YAML file"]) --> SPEC["<b>Spec</b> and the <b>Program</b> it compiles to<br/><i>checked before any data exists</i>"]
SPEC --> CHECK["<b>check it</b><br/>python -m mathspec check"]
SPEC --> SHOW["<b>print it as math</b><br/>LaTeX · Typst · Markdown"]
SPEC -.-> OTHER["<b>your own tool</b><br/>a renderer · a checker · …<br/>reads the Program API"]
SPEC -.-> ENGINE["<b>an engine</b><br/>specsolve<br/>builds the model for a solver"]
DATA[("your data")] -.-> ENGINE
ENGINE -.-> ANS(["your answers"])
classDef outside stroke-dasharray:5 4
class ENGINE,DATA,ANS,OTHER outside
Background#
mathspec started life as YAML math in the energy system modelling framework Calliope. The Calliope maintainers developed many of the language elements mathspec uses today, from initial concepts in 2020 to their first stable release using 100% YAML math in September 2026.
mathspec is an evolution of the initial Calliope work, ported to a framework-agnostic organisation and now developed as a cross-framework initiative. This initiative has proven a success, with improvements in syntax, performance, and features realised already in the first months. Our aim is to develop a human-readable math language that can be used across the energy modelling community (and beyond!) to increase transparency, shareability, and readability of our math.
For more information see our Background and history page.
Where to next#
- Tutorials teach the language in three lessons, and in the first you build the file above one section at a time.
- How-to guides list the steps for one task, such as checking a spec without data or printing it as math.
- Reference describes what a file may contain and what it means, key by key, with whole specs beside their math.
- About explains why the language has the shape it has.
- Development covers how an engine or a renderer reads a spec, and how to contribute to mathspec.
Install it#
Install mathspec from PyPI with pip, uv or pixi.
Alpha software
mathspec is alpha software and makes no compatibility promise yet. Breaking changes land without a deprecation cycle. If you depend on mathspec, pin an exact version, and read the changelog before you upgrade. The test suite passes every construct through the schema, the parsers and all three typeset formats, and compiles the LaTeX.
