math_spec.degree
Degree — the clause of the expressive ceiling that is a scope choice.
Decidable on the resolved core AST with no data bound, which is what makes
lps.check() a real gate rather than a syntax pass, and the one
admissibility rule that is a scope choice rather than a consequence of
streaming (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 by something
downstream.
Where a quadratic model can land is a different axis, declared by each
consumer and answered by check(model, sink=...). This module says what is
sayable and stops there; refusing degree 2 outright was letting one library's
limits read as a rule about math.
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 every term of one against every
term of the other, a cross join whose size the file states nowhere — the one
shape "bilinear" hides that the ceiling doc genuinely excludes, and the
boundary linopy's own * draws, which is what keeps hard rule 3 structural
rather than lucky. Factors carrying different dims are not that: x[i] *
y[j] broadcasts, and x[i] * y[j] * a[i, j] joins through a declared table.
That is why it lives here and not in lowering.py: degree is a property of
the language, so both lanes give the same verdict and the same sentence, as
they do for dim sets and the closed operator set. Stated once, every consumer
asks — a copy in a lane is the spelling no differential test covers.
A divisor's shape is decided here too. A quotient is built as
multiplication by one reciprocal factor, and an addition is what makes a
variable-free expression more than one — so a sum divisor is refused, and
refused at load, where the sentence can name the rewrite. Not a degree
question (x / (a + b) is affine) but the same node, the same verdict owed
to both lanes, and the same answer with no data bound.
Deliberately narrow: :func:check_binary decides a binary operator node, the
only place degree can be lost, and :func:check_expression is that decision
over a whole expression — for the formulations, which judge a link before there
is a declaration to name in the error.
ARITHMETIC_OPERATORS = frozenset({'+', '-', '*', '/', '**'})
module-attribute
#
carries_variable(node)
#
Whether node contains a decision variable.
A structural question over the resolved AST — no data, no plan. A
NameNode reaching here is a resolution bug rather than a false
negative, so it is refused rather than silently answered.
Source code in src/math_spec/degree.py
check_binary(node, context=None, *, ceiling=1)
#
Check that node stays inside the degree its position allows.
Callers want the raise, not the answer — the same shape as
dimensions.dims_of being asked for its verdict.
| PARAMETER | DESCRIPTION |
|---|---|
node
|
The product, quotient or sum to judge.
TYPE:
|
context
|
What to name in the message — the declaration being lowered.
TYPE:
|
ceiling
|
The highest degree this position can honour — 2 in an objective, 1 everywhere else, and the module docstring is why.
TYPE:
|
| 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, or an operator the language does not have. |
Source code in src/math_spec/degree.py
check_expression(node, context, *, ceiling=1)
#
Apply :func:check_binary everywhere in node.
Lowering asks per node as it descends, because it is already walking. A
caller that only wants the verdict on an expression it holds asks here,
and gets the identical sentence — which is the point: piecewise:
judges its link expressions this way so that p * p is refused against
the link the user wrote, not against curve_link0, the declaration
the expansion went on to generate.
Degree only, deliberately. What a plan node can represent is a different question and a consuming lane's to ask; a formulation runs in lanes that build no plan at all.
Source code in src/math_spec/degree.py
is_quadratic(node)
#
Whether node multiplies two variable-carrying operands.
What :func:check_binary refuses at ceiling=1, asked of a whole
expression rather than of one node — by a consumer that has to build the
thing rather than judge it, and cannot build this one. Whether an
expression is quadratic is a fact about the expression; what that costs a
consumer is the consumer's own to declare.
A second home reads the same question off the plan rather than the AST. Two, because the two representations are different types and the lanes share neither.