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 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)

where_dims(node, schema) #

The dims a resolved predicate reads.

A where: is checked against a declared frame; a checks: block has no frame of its own, so its rows are exactly the coordinates its names span, and this is what says which those are.

Source code in src/math_spec/dimensions.py
def where_dims(node: WhereNode | None, schema: Spec) -> frozenset[str]:
    """The dims a resolved predicate reads.

    A ``where:`` is checked *against* a declared frame; a ``checks:`` block has
    no frame of its own, so its rows are exactly the coordinates its names
    span, and this is what says which those are.
    """
    if node is None or isinstance(node, BooleanLiteralNode):
        return frozenset()
    if isinstance(node, (ParameterDefinedNode, ParameterComparisonNode)):
        return frozenset(schema.parameters[node.name].dims)
    if isinstance(node, VariableDefinedNode):
        return frozenset(schema.variables[node.name].foreach)
    if isinstance(node, (DimensionComparisonNode, DimensionPositionNode)):
        return frozenset({node.name})
    if isinstance(node, (LookupComparisonNode, LookupPairComparisonNode, LookupDefinedNode)):
        return frozenset({node.over})
    if isinstance(node, NotNode):
        return where_dims(node.operand, schema)
    if isinstance(node, (AndNode, OrNode)):
        return where_dims(node.left, schema) | where_dims(node.right, schema)
    msg = f'{type(node).__name__} reached the dim reader unresolved.'
    raise AssertionError(msg)