Skip to content

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.

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

Start the tutorial See the examples Read the language


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 snapshot and generator
  • the parameters it takes as data, such as load and cost
  • 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.

dispatch.yaml
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

\[ 0 \le \mathit{dispatch}_{g,s} \le \bar p_{g} \qquad \forall\, g \in \mathcal{G},\ s \in \mathcal{S} \,:\, \bar p_{g} > 0 \]

Objective#

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

Subject to#

power_balance

\[ \sum_{g \in \mathcal{G}} \mathit{dispatch}_{g,s} = \ell_{s} \qquad \forall\, s \in \mathcal{S} \]
\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.

Contributors#

Bryn Pickering
Bryn Pickering

💻 📖 🤔 📆 🚧 👀 🚇
Felix Bumann
Felix Bumann

💻 📖 ⚠️ 🚇 🚧 🤔 👀
Fabian Hofmann
Fabian Hofmann

💻 📖 🐛 🤔 👀 🚧
Jonas Hörsch
Jonas Hörsch

💻 🤔 👀
Stefan Pfenninger-Lee
Stefan Pfenninger-Lee

🤔 👀
Ivan Ruiz Manuel
Ivan Ruiz Manuel

🤔 👀
Tim Tröndle
Tim Tröndle

🤔
Jann Launer
Jann Launer

🤔 🐛
Francesco Lombardi
Francesco Lombardi

🐛
James Morris
James Morris

🐛
Stefan Strömer
Stefan Strömer

🐛
Alexander de Tomás Pascual
Alexander de Tomás Pascual

🐛