Skip to content

math_spec.degree

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

Degree 2 in the math, degree 1 in what stands beside it. An objective and a constraint both take variable * variable; a bound and a piecewise: link do not — each of those is read affinely. An expressions: entry is not degree-checked at declaration at all: the math reading it is checked where it reads, at that position's own ceiling, and an entry the math never reads (ExpressionDeclaration.in_math) is held to no degree.

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.

calls_dual(node) #

Whether a :class:DualNode stands anywhere in the resolved node.

Source code in src/math_spec/degree.py
def calls_dual(node: ParsedNode) -> bool:
    """Whether a :class:`DualNode` stands anywhere in the resolved *node*."""
    return any(isinstance(found, DualNode) for found in nodes(node))

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.
    """
    for found in nodes(node):
        if isinstance(found, UnresolvedNode):
            msg = f'{found!r} reached the degree check. Expressions go through resolution.resolve_expression() first.'
            raise AssertionError(msg)
        if isinstance(found, VariableNode):
            return True
    return False

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

What the math admits at one position: no dual() anywhere under node, then :func:check_binary everywhere in it.

Asked of the expanded tree, so a dual or a product inlined through a macro or a named expression is caught alongside one written in place. What a plan node can represent is the consumer's question, not this one's.

RAISES DESCRIPTION
LanguageError

A dual, which exists only after a solve; or what :func:check_binary refuses.

Source code in src/math_spec/degree.py
def check_expression(node: ParsedNode, context: str, *, ceiling: int = 1) -> None:
    """What the math admits at one position: no ``dual()`` anywhere under *node*, then :func:`check_binary` everywhere in it.

    Asked of the *expanded* tree, so a dual or a product inlined through a
    macro or a named expression is caught alongside one written in place.
    What a plan node can represent is the consumer's question, not this one's.

    Raises:
        LanguageError: A dual, which exists only after a solve; or what
            :func:`check_binary` refuses.
    """
    for found in nodes(node):
        if isinstance(found, DualNode):
            raise LanguageError(
                f'{context}: a dual exists only after a solve; the math cannot read one — '
                f'keep the entry that carries it out of constraints, the objective, bounds and where.'
            )
        if isinstance(found, BinaryOperatorNode):
            check_binary(found, context, ceiling=ceiling)