Skip to content

math_spec.dimensions

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 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), schema, 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), 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.

RAISES DESCRIPTION
DimensionError

On the first rule broken.

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)