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

See the examples Read the language


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.

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 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 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#

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.