Errors and limits#
to_spec is the check#
There is one entry point, and it binds nothing. ms.to_spec('model.yaml')
parses the file, expands every piecewise: block, resolves every name,
checks every dim rule and every degree, and reads every where string and
every macro template — the uncalled ones included — before it returns a
Spec. Anything the language refuses is refused there, so a repository of
models is validated in CI with no data and no solver, and the worst error a
consumer downstream could hand you — an opaque array or solver exception with
no pointer back to a declaration — cannot be one of this package's.
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.
advice is what is decidable and not an error#
Two more things are decidable without data, and each is advice rather than a
refusal. ms.advice(model) returns both as a tuple of ms.Advice, each with a
kind (one of ms.ADVICE_KINDS: never-an-axis or unbounded), the subject declaration it is about,
and its text — str() of one is the sentence. A consumer prints them, or
filters on the two fields; the sentences are the language's, so no consumer
writes its own.
From a shell, python -m math_spec check model.yaml is the two together: a
refusal is its message on stderr and exit status 1, advice is printed and the
status is 0.
A dimension nothing is indexed by and nothing aggregates into is never an axis. Where a lookup targets it, it is a label space wearing a dimension's declaration, and the note says how to declare it as one; where nothing reaches it at all, it is unused.
A variable that no constraint names, and whose bounds leave open the side its
objective term improves toward, runs to infinity for every dataset there is. A
solver says that with a bare unbounded naming nothing; the note says it with
the variable and the side:
Variable 'slack' makes this model unbounded: no constraint names it, and
bounds.lower is -inf, which is the direction a +slack term improves a minimize
objective in. No data can change that, so the solve would answer `unbounded`
and name nothing.
Give it a finite bounds.lower, or the constraint that was meant to define it.
Advice, because the same shape is what a half-written model looks like — a
variable declared before the constraint that will hold it — and to_spec
stays open to one. It is a list a consumer asks for, not an error it is
handed: build straight from the model and the solver's bare answer is still
the first word.
Both halves of the conjunction are needed, and neither alone is wrong: a
variable held by nothing but its own bounds: is ordinary, and so is an
unbounded one that a constraint names. Where the sign a variable enters the
objective with is data — a parameter coefficient, which may be zero or either
sign — nothing is said, because a note against a model that solves is the worse
error. The per-coordinate case, where a where: mask leaves one slice of a
variable with no constraint row, is not decidable from the file
(#229).
Which error you get#
MathSpecError |
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 |
Every one of them is the file being wrong, and every one is reproducible from
the YAML alone — no data, no solver. That is the whole tree this package
raises: a consumer that binds numbers or calls a solver adds its own errors
below MathSpecError, and says so in its own documentation.
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 in a bound, a named expression or a piecewise: link |
the objective and constraints take it; elsewhere, a parameter coefficient (expressions) |
sum(x, over=d) * sum(y, over=d) |
multiply before reducing, or name the reduction with a variable — a product of two sums is a cross join |
degree 3 (x * y * z) |
a variable constrained to equal one product, then multiplied by the third |
** |
x * x (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 |
| indicator constraints | not a language question: what a consumer can take is its own axis, the one 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.
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.