Run a model¶
A model is two things: a YAML file, and a mapping of the names it declares to your tables. This page is the path from those two to an answer you can read back. What may be in the file is the language, and the language is documented with itself — it is a package this one depends on, not a chapter of this site.
If you would rather see the machinery than read about it,
python examples/walkthrough.py prints every stage — YAML → schema → AST →
plan → frames → LP text → solution — for one small model. And once a model
exists, Change a model is the notebook loop — new numbers,
more rows, new math, and how to tell which of the three you are paying for.
Every output on that page is the site build's own run.
The language, in five links¶
Enough to read a model file, each shown in a model that lives in the repo and is run by the test suite:
| A dimension is an axis, and its coordinates usually come from the data. One master set per dimension, resolved before anything binds, so two tables disagreeing about which snapshots exist is a load-time error rather than a truncated model. | dimensions · dispatch |
Absence is how you say "sparse". A where: does not zero a variable out — the variable has no column there at all, and the built model is smaller than the coordinate product. |
absence · dispatch |
A lookup maps one dimension onto another, and that is your topology. sum(p, by=gen_bus) lands on the dimension the lookup points at; no adjacency matrix and no join written by hand. |
lookups · transport |
shift reaches along an axis, and edge= says what happens at the boundary — 'wrap' is what makes a battery cyclic without writing the boundary condition out. |
shift · storage |
The dims of an equation must equal its foreach. Get it wrong and you are told at load time: a stray dim would multiply rows, an unused one would repeat a row across them. |
dim algebra · monthly budget |
Check, build, solve¶
import lpspec as lps
lps.check('model.yaml') # compiles? no data needed
sol = lps.solve('model.yaml', sources) # to an answer
sol.objective
sol.primal('p') # a polars.DataFrame
sol.dual('power_balance')
sol.activity('power_balance') # the row's left-hand side at the solution
lps.check is the CI verb — it parses, expands, resolves and lowers without
binding anything, so a model repository can be validated on every commit
without shipping the data. With sink= it also answers the second question:
will that solver take this model.
Between the two sits lps.build(model, sources), which binds and builds
without solving — what you want when the same built model is solved many times
with new numbers, and what rebind
re-uses. → Python API
Your numbers go in as tables¶
sources maps each declared name to a parquet path, or any table exposing
the Arrow PyCapsule protocol — polars, pandas, pyarrow, duckdb — and the
recogniser imports none of them on your behalf. Results come back as frames;
to_pandas, to_dataarray and to_parquet are the bridges out.
Two pages cover the whole of it: preparing the data is the journey from the files an instance actually arrives in to that mapping, and the data contract is what binds, what is refused, and which sentence you get when it is.
Editor completion and offline checking¶
The YAML surface ships as a JSON Schema —
schema/math-spec.schema.json,
generated from the same declarations lps.check validates against and held
current by a test. It travels with the language, so the examples below read it
from math-spec over the network; a vendored copy takes a path in the same slot.
With
the Red Hat YAML extension
it gives key completion, hover docs, the closed vocabulary behind dtype:,
domain:, sense: and the rest, and a squiggle on a misspelled key — before Python runs. Map it per workspace:
// .vscode/settings.json
"yaml.schemas": { "https://raw.githubusercontent.com/energy-models/math-spec/main/schema/math-spec.schema.json": ["*.model.yaml"] }
or per file, with a modeline on the first line:
# yaml-language-server: $schema=https://raw.githubusercontent.com/energy-models/math-spec/main/schema/math-spec.schema.json
The same file checks a model without Python, which is what a pre-commit hook or a non-Python CI job wants:
uvx check-jsonschema --schemafile https://raw.githubusercontent.com/energy-models/math-spec/main/schema/math-spec.schema.json model.yaml
It validates structure only. expression: and where: are strings to the
schema, so everything inside them — the actual math — is checked by
lps.check, not by the schema.
What it will not do¶
Worth knowing before you start, rather than after:
- Bounds take a name or a number, never arithmetic.
upper: p_maxis fine;upper: -ratingis not. This one has bitten a real port — #31, and the workaround is to ship the negated column as data. - The math takes degree 2; what stands beside it does not. The objective
and constraints take
variable * variable; a bound, a named expression and apiecewise:link need a variable-free factor. Where a quadratic model can be solved is a second question —check(model, sink=…)answers it. → The ceiling - Several plausible features are refused on purpose, with reasons. → the roadmap
Where next¶
| Preparing the data · the contract | files to frames, and what binding refuses |
| Python API · Sweeps | building, solving, reading back; and solving once per slice |
| Examples | every model in the repo, and which constructs each exercises |
| Language reference | what a file may contain, exactly |
| Typeset the math | the same file as LaTeX, Typst or Markdown |
| About | why it is shaped this way, what it costs, where it is going |