Static dim-set checking — a type system whose type is a set of dim names.
Parameter dims are declared, variable foreach is declared, and operator
dimension arguments are name-checked, so every node's dim set is computable
before any data is bound. That is the whole basis of this pass: it runs at
load time, on the resolved core AST, so every consumer gets the same answer by
construction. The per-node rules are the "Dim algebra" table in
docs/reference/language/expressions.md; at the declaration level::
constraint -> the dims of both sides together must *equal* foreach
where -> the predicate's dims must not exceed the frame
bounds -> the bound parameter's dims must not exceed foreach
The direction that matters most is the stray dim: one the frame does not
declare broadcasts silently at build time, so the same YAML quietly builds a
bigger model than it reads as. The missing direction is checked too, a foreach
dim the equation never uses just repeating one row across it — nearly always a
typo.
check_schema(schema)
Check every declaration's dim rules.
| RAISES |
DESCRIPTION |
DimensionError
|
On the first declaration that breaks one.
|
Source code in src/math_spec/dimensions.py
| def check_schema(schema: Spec) -> None:
"""Check every declaration's dim rules.
Raises:
DimensionError: On the first declaration that breaks one.
"""
ns = Namespace.of(schema)
for vname, vdef in schema.variables.items():
frame = frozenset(vdef.foreach)
context = f"Variable '{vname}'"
_check_where_dims(where_of(vdef.where, ns, context), schema, frame, context)
for side in ('lower', 'upper'):
bound = getattr(vdef.bounds, side)
if isinstance(bound, str):
bdims = frozenset(schema.parameters[bound].dims)
if not bdims <= frame:
raise DimensionError(
f"{context}: bounds.{side} parameter '{bound}' has dims "
f"{sorted(bdims - frame)} outside the variable's foreach "
f'{sorted(frame)}.'
)
for cname, cdef in schema.constraints.items():
frame = frozenset(cdef.foreach)
context = f"Constraint '{cname}'"
_check_where_dims(where_of(cdef.where, ns, context), schema, frame, context)
got = dims_of(expression_of(cdef.expression, schema, ns, context), schema, context)
if got != frame:
stray, missing = sorted(got - frame), sorted(frame - got)
detail = (
f'carries dims {stray} that are not in foreach {sorted(frame)} — every '
f'stray dim multiplies the rows this constraint builds; add it to '
f'foreach if that is intended, or sum it out'
if stray
else f'does not carry {missing}, which foreach declares — the same row '
f'would be repeated across {missing}; drop it from foreach, or use it '
f'in the expression'
)
raise DimensionError(f'{context}: the expression {detail}.')
if schema.objective is not None:
context = 'The objective'
got = dims_of(expression_of(schema.objective.expression, schema, ns, context), schema, context)
if got:
raise DimensionError(
f'{context}: the expression carries dims {sorted(got)}, and an objective is one '
f'number. Wrap each additive term in its own sum(): '
f'`sum(p * cost) + sum(p_nom * capex)`.'
)
|
dims_of(node, schema, context)
The dim set of a resolved expression, checking every rule on the way.
Source code in src/math_spec/dimensions.py
| def dims_of(
node: ExpressionNode,
schema: Spec,
context: str,
) -> frozenset[str]:
"""The dim set of a resolved expression, checking every rule on the way.
Raises:
DimensionError: On the first rule broken.
"""
if isinstance(node, ComparisonNode):
return _dims(node.left, schema, context) | _dims(node.right, schema, context)
return _dims(node, schema, context)
|