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), 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 ename, block in schema.expressions.items():
if not block.cases:
continue
frame = frozenset(block.foreach or [])
for case_name, case in block.cases.items():
context = case_context(ename, case_name)
_check_where_dims(where_of(case.when, ns, context), frame, context)
_check_value_dims(case.expression, schema, ns, frame, context)
assert block.otherwise is not None
_check_value_dims(block.otherwise, schema, ns, frame, case_context(ename, None))
for cname, cdef in schema.constraints.items():
frame = frozenset(cdef.foreach)
context = f"Constraint '{cname}'"
_check_where_dims(where_of(cdef.where, ns, context), 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)`.'
)