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:~math_spec.program.Dual stands anywhere under node.

Source code in src/math_spec/degree.py
def calls_dual(node: Expression) -> bool:
    """Whether a :class:`~math_spec.program.Dual` stands anywhere under *node*."""
    return any(isinstance(found, Dual) for found in walk(node))

check_binary(node, context, *, ceiling) #

Check that node stays inside the degree its position allows.

PARAMETER DESCRIPTION
node

The product, quotient or power to judge.

TYPE: Multiply | Divide | Power

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: Multiply | Divide | Power, context: str, *, ceiling: int) -> None:
    """Check that *node* stays inside the degree its position allows.

    Args:
        node: The product, quotient or power 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 isinstance(node, Power):
        if carries_variable(node):
            raise LanguageError(_a_variable_under_a_power_message(where))
        if _adds(node.base) or _adds(node.exponent):
            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.'
            )
        return
    if isinstance(node, Divide):
        if carries_variable(node.divisor):
            raise LanguageError(
                f'{where}the divisor contains variables, which is not affine. '
                f'Divide by a parameter, or precompute the reciprocal as one.'
            )
        if _adds(node.divisor):
            raise LanguageError(
                f'{where}a divisor must be a single Constant/Parameter factor, '
                f'not a sum — rewrite as multiplication by a precomputed parameter'
            )
        return
    if 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 resolved tree, so a dual or a product reached 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: Expression, 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 resolved tree, so a dual or a product reached 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 walk(node):
        if isinstance(found, Dual):
            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, Multiply | Divide | Power):
            check_binary(found, context, ceiling=ceiling)