Skip to content

Errors and limits

Everything decidable without data is decided without data

Anything detectable before building is detected before building. The worst error this language could hand you is an opaque solver or array exception with no pointer back to a YAML declaration, so a model is parsed, expanded, resolved and dim-checked — including uncalled macro templates and every where string — before a single source is read.

lps.check('model.yaml') runs exactly that and binds nothing, which is why it is the CI verb: a model repository can be validated on every commit without shipping the data.

Every message names what went wrong, what to do about it, and where it helps, the valid options:

Constraint 'balance', equation 0: 'p_charge' not found.
  Variables: ['p', 'soc']
  Parameters: ['p_max', 'load', 'efficiency']
Check for typos, or ensure 'p_charge' is declared.

A construct outside the language names the construct and its rewrite, never a silent fallback.

Which error you get

LpspecError the root of the tree; everything below is an instance of it
LanguageError the model: a construct outside the language, a dim set that does not compose, a name nothing declares
SchemaError the file: an unknown key, a malformed declaration, a bad symbol table
DimensionError dims that disagree — a constraint whose expression does not equal its foreach
PiecewiseExpansionError a piecewise: block that cannot be expanded
DataError what was bound: a missing source, an unreadable one, a coordinate outside the master index

The split is the useful one for a caller: LanguageError and its subclasses are the file being wrong, and are reproducible from the YAML alone; DataError is the numbers being wrong for a file that is fine.

check also issues an LpspecWarning for advice short of an error — a declared dimension nothing uses as an axis, say. It is the only place warnings come from.

What the language will not say

Refusals, and what to reach for instead. None of them is an unimplemented feature list: each is a boundary the design keeps on purpose, and the ceiling is the argument for where it sits.

Not here Instead
variable × variable, or ** nothing — degree 1 is the ceiling (expressions)
arithmetic in bounds: a name or a number; ship the derived column as data (#31)
time-series processing (resample, cluster, interpolate, align), file IO, units data prep; pass a parameter
solver breadth two solvers — HiGHS, which ships, and Gurobi via the [gurobi] extra — chosen at the call and never in the file; LP files for everything else (#106)
indicator constraints planned as a solver capability rather than a language question, the same axis sos: landed on (#220)
multi-objective one objective: block — a second is unsayable; weight them into one expression
arbitrary array ops (merge, reindex, apply_ufunc) data prep — the closed operator set is what makes streaming possible
filling a missing value (.fillna) data prep, or a where if you meant the coordinate not to exist. In the language only where the data cannot reach: shift(..., edge=) (absence)
schema migrations —

A model built partly in Python has no readable .yaml representation and will not get one: the math side is feasible, but expression and where strings come back as anonymous arrays, so the round trip would be functional and not reviewable — which is the whole point of the file. A framework that wants to emit declarations passes a dict, and gets to_yaml() back (Python API).

Where the language genuinely cannot say the math, the escape hatch is a declared escape: island — named in the file, bounded by the preceding where mask, terminal, and billed against a label budget before any Python runs. It is #38 and not shipped.