Skip to content

math_spec.degree

Degree — the one admissibility rule that is a scope choice (docs/about/ceiling.md).

Degree 2 in the math, degree 1 in what stands beside it. An objective and a constraint both take variable * variable; a bound, a named expression and a piecewise: link do not — each of those is read affinely.

A degree-2 product has a second rule: at most one factor may be a sum of terms. sum(x, over=i) * sum(y, over=j) is a cross join whose size the file states nowhere. Factors carrying different dims are not that: x[i] * y[j] broadcasts.

A divisor's shape is decided here too: a quotient is multiplication by one reciprocal factor, so a divisor that adds is refused at load, where the message can name the rewrite.

carries_variable(node) #

Whether node contains a decision variable, over the core AST.

:func:math_spec.program.carries_variable answers the same question over a program. An unresolved node reaching here is a resolution bug, so it is refused rather than silently answered.

Source code in src/math_spec/degree.py
def carries_variable(node: ParsedNode) -> bool:
    """Whether *node* contains a decision variable, over the core AST.

    :func:`math_spec.program.carries_variable` answers the same question over a
    program. An unresolved node reaching here is a resolution bug, so it is refused
    rather than silently answered.
    """
    if isinstance(node, VariableNode):
        return True
    if isinstance(node, NumberNode | ParameterNode | KwargNode):
        return False
    if isinstance(node, UnresolvedNode):
        msg = f'{node!r} reached the degree check. Expressions go through resolution.expression_of() first.'
        raise AssertionError(msg)
    if isinstance(node, BranchNode):
        return any(carries_variable(c) for c in children(node))
    assert_never(node)

check_binary(node, context, *, ceiling) #

Check that node stays inside the degree its position allows.

PARAMETER DESCRIPTION
node

The product, quotient or sum to judge.

TYPE: BinaryOperatorNode

context

What to name in the message — the declaration being read.

TYPE: str

ceiling

The highest degree this position can honour — 2 in an objective or a constraint, 1 everywhere else.

TYPE: int

RAISES DESCRIPTION
LanguageError

A product of two variable-carrying factors where the position allows only degree 1 or where both factors are sums of terms, a power over anything carrying a variable, a divisor carrying a variable or adding.

Source code in src/math_spec/degree.py
def check_binary(node: BinaryOperatorNode, context: str, *, ceiling: int) -> None:
    """Check that *node* stays inside the degree its position allows.

    Args:
        node: The product, quotient or sum to judge.
        context: What to name in the message — the declaration being read.
        ceiling: The highest degree this position can honour — 2 in an
            objective or a constraint, 1 everywhere else.

    Raises:
        LanguageError: A product of two variable-carrying factors where the
            position allows only degree 1 or where both factors are sums of
            terms, a power over anything carrying a variable, a divisor carrying
            a variable or adding.
    """
    where = f'{context}: ' if context else ''
    if node.op == '**':
        if carries_variable(node):
            raise LanguageError(_a_variable_under_a_power_message(where))
        if _adds(node.left) or _adds(node.right):
            raise LanguageError(
                f'{where}a base and an exponent must each be a single Constant/Parameter factor, '
                f'not a sum — addition does not distribute over `**`, so `(1 + rate) ** period` is '
                f'refused where `growth ** period` is not. Bind the factor itself.'
            )
    if node.op == '/' and carries_variable(node.right):
        raise LanguageError(
            f'{where}the divisor contains variables, which is not affine. '
            f'Divide by a parameter, or precompute the reciprocal as one.'
        )
    if node.op == '/' and _adds(node.right):
        raise LanguageError(
            f'{where}a divisor must be a single Constant/Parameter factor, '
            f'not a sum — rewrite as multiplication by a precomputed parameter'
        )
    if node.op != '*' or not (carries_variable(node.left) and carries_variable(node.right)):
        return
    if ceiling < 2:
        raise LanguageError(_degree_two_here_message(where))
    if (degree := _degree(node)) > ceiling:
        raise LanguageError(_above_the_ceiling_message(where, degree))
    _check_single_term_factor(node, where)

check_expression(node, context, *, ceiling=1) #

Apply :func:check_binary everywhere in node.

Degree only, deliberately: what a plan node can represent is a consuming lane's question.

Source code in src/math_spec/degree.py
def check_expression(node: ParsedNode, context: str, *, ceiling: int = 1) -> None:
    """Apply :func:`check_binary` everywhere in *node*.

    Degree only, deliberately: what a plan node can represent is a consuming
    lane's question.
    """
    if isinstance(node, BinaryOperatorNode):
        check_binary(node, context, ceiling=ceiling)
    for child in children(node):
        check_expression(child, context, ceiling=ceiling)