Typeset the math#
A model is a declaration, so it can be printed the way a paper prints it — from the file itself, with no data and no solver. It is the cheapest review tool available for "does this YAML say what I meant", and it is how a model states its math with nothing but the file.
Every construct the language has, beside the math it prints, is one page: Every construct, as math — which is where to look when the question is whether the notation is right, rather than how to print it.
import math_spec as ms
spec = ms.to_spec('model.yaml') # read and checked once, then printed three ways
print(ms.to_latex(spec)) # amsmath align
print(ms.to_typst(spec)) # compiles without a TeX toolchain
print(ms.to_markdown(spec)) # renders as-is on GitHub
Each of the three takes what to_spec takes — a path, the YAML, a mapping —
and reads it. Hand it the Spec instead and the file is read and checked once
rather than once per format, which is also how a Spec you already hold gets
printed without a second trip through the loader.
Or from a shell, where this belongs in a Makefile next to pdflatex:
python -m math_spec latex model.yaml --symbols model.symbols.yaml --standalone -o model.tex
python -m math_spec typst model.yaml --standalone -o model.typ
python -m math_spec markdown model.yaml
Options#
The three functions take the same keywords; the CLI spells each as a flag.
symbols |
--symbols FILE |
how names should print — below. Default: derived |
standalone |
--standalone |
emit a document that compiles, rather than a fragment to include. Default: fragment |
legend |
--no-legend |
the sets / parameters / variables table above the math. Default: on |
numbered |
--no-numbers |
number the equations. Default: on |
-o FILE writes to a file instead of stdout.
The model's own description: opens the document either way — it is what the
file says it is, not a symbol table. A piecewise: block prints as the
λ-formulation it expands to rather than the sugar it was written as, because
that is the math the solver receives. Where the math translates an index —
shift, in any of its edge spellings — the document also prints a line saying
what the notation for it means, so a reader meets no symbol the page has not
defined.
A model that does not compile does not print: typesetting runs the same load-time checks everything else does.
It does not line-break. A wide equation runs off the page; that is a formatting decision this package does not make for you.
Symbol tables#
With no table, symbols are derived — unambiguous rather than beautiful, so
a model prints with no setup at all: \(\mathit{load}_t\), \(p^{\mathrm{max}}_g\). A
SymbolTable makes it conventional:
symbols = {
'notation': 'latex',
'dimensions': {
'snapshot': {'index': 's', 'set': '\\mathcal{S}'},
'generator': {'index': 'g', 'set': '\\mathcal{G}'},
},
'names': {
'cost': 'c',
'load': '\\ell',
'p_max': '\\bar p',
},
}
ms.to_latex('dispatch.yaml', symbols=symbols)
A dict, a YAML path, or a ms.SymbolTable. As a sidecar file:
# dispatch.symbols.yaml — not a model, so nothing here is checked against the schema
notation: latex
dimensions:
snapshot: { index: s, set: "\\mathcal{S}" }
generator: { index: g, set: "\\mathcal{G}" }
names:
cost: c
load: "\\ell"
p_max: "\\bar p"
| Section | |
|---|---|
notation |
required — latex or typst, the language the entries are written in |
dimensions |
per dimension, an index letter and a set symbol; either may be omitted |
names |
per parameter or variable, its symbol |
Every spelling is printed verbatim. Nothing parses or translates notation,
which is why notation: is required and why rendering a LaTeX table as Typst
refuses rather than producing something that nearly works.
A key naming nothing in the model is an error, with the near miss — not a symbol that silently never applies and a reader who never finds out.
Presentation is not language. Nothing in a symbol table changes what the
file means, no solver reads it, and what a declaration is stays the model's
own description: (declarations), which travels
with the declaration and reaches every consumer.