immut/term design
Design goal
TermPolynomial[A] is the multivariate polynomial in distributed form: a flat, sorted list of non-zero terms. It is the representation for traversing a polynomial in order, for whole-polynomial arithmetic, and for reading the leading term directly, while staying an immutable value with structural equality.
Mathematical background
A polynomial is a finite sum with distinct exponent vectors and non-zero coefficients (see the core design). Fix the monomial order of ExponentVector::compare. Listing the terms so that gives the canonical term list of . The first term is the leading term , the leading monomial and the leading coefficient.
Since is total and the are distinct, the sorted list exists and is unique, so
Design decisions
Canonical form: sorted, merged, without zeros
Choice. A TermPolynomial stores exactly the canonical term list, in a persistent @immut/vector.Vector. The derived Eq compares the stored lists, which by the equivalence above is polynomial equality. from_terms brings any input into this form in three steps:
- sort a copy of the input in descending order by
compare; - walk the sorted list and add the coefficients of each run of equal exponent vectors;
- keep a run only if its sum is non-zero.
Equal vectors are adjacent after step 1 because compare returns exactly for equal vectors. Step 2 sums a run in whatever order the sort left it, which is harmless because AddMonoid addition is associative and commutative. Step 3 is what makes disappear. Sorting costs comparisons, each for vectors of length .
Arithmetic by concatenation and renormalization
Addition concatenates the two term lists and calls from_terms; multiplication forms all products and calls from_terms. Both are the textbook definitions followed by the canonicalization above, so their correctness reduces to the correctness of from_terms. The costs are and comparisons.
Scaling and negation keep the order
scale(γ, c) maps every term to and does not re-sort. This is sound because the map is strictly increasing for a monomial order:
(compatibility, derived in the core design). Strictly descending input therefore gives strictly descending output, and in particular distinct exponents stay distinct. Coefficients can still vanish when has zero divisors (for Int, ), so such terms are filtered out. The result is canonical in term operations. Negation keeps the exponents, so it only filters.
The leading term of a product
Storing terms in descending order makes the leading term to_terms()[0], and the monomial order makes it multiplicative. Let and be the leading monomials of and . For any terms and ,
applying compatibility once with and once with ; both steps are equalities only when and . So arises from exactly one pair of terms, its coefficient in is , and
This is the property that makes graded orders useful for division-style algorithms, and the reason the representation commits to a monomial order rather than an arbitrary key order.
Evaluation term by term
eval(values) computes , each power by binary exponentiation. For a commutative coefficient ring this is the evaluation homomorphism , ; it is a ring homomorphism by the same monomial argument as in the dense design. The cost is coefficient multiplications.
The point must supply at least arity() values: index is read for every variable a term uses. eval aborts on a shorter array and eval_checked returns None; values beyond the arity are never read.
Shape and capability metadata
arity is the longest stored exponent vector, total_degree the largest term degree, and term_count the list length; together they form PolynomialShape::Multivariate. All three are computed by one pass over the terms; nothing is cached, because the list is never modified after construction.
Correctness / invariants
- Canonical form. Terms are strictly descending in and every coefficient is non-zero; every constructor and operation re-establishes this, either through
from_termsor by the order-preservation argument forscaleandneg. - Equality of values is equality of polynomials.
- Leading term.
to_terms()[0]is , and when the coefficient product is non-zero. one()is the zero polynomial exactly when inA.- Powers.
pow(e)performs multiplications andpow(0)isone(). - Complexity (, terms):
from_terms;+;*;scale,neg,arity,total_degree;eval. Each comparison costs .
Alternatives rejected
- Heap-based multiplication (merging the sorted streams with a priority queue) would lower multiplication to and avoid materializing all products. The current version keeps the simpler sort-based normalization that every operation shares.
- Merge-based addition of two sorted lists would be linear; it is not implemented for the same reason.
- Recursive (nested univariate) representation, , makes multivariate Horner evaluation natural but ties the layout to a variable order and makes the leading term of the distributed order expensive to find.
- Ascending order. The leading term would be the last element. Descending order shows the highest-degree part first, which matches how polynomials are usually written.
Boundaries
- No division, normal forms, S-polynomials or Gröbner bases, although the monomial order would support them.
- No exponent lookup by key; use
SparsePolynomialwhen you needgetby exponent vector. - Variables are positional; names are the job of
ContextPolynomial. - No
CompareorHashinstance, so term polynomials cannot be keys of sorted or hash maps. - Exponents are
UIntand wrap on overflow; coefficients follow the semantics ofA(wrapping integers, rounding floats, exact== 0trimming).