Tutorials

Exact algebra with polynomial arrays

Compute GCDs, factor polynomials, recover exact coefficients from samples, and change variables through poly.dense and poly.sparse. Start with coefficient or term sequences; the functions infer domains and return arrays you can use in the next calculation. If these inputs are new to you, begin with polynomial arrays.

Inspect sparse terms

Use sparse.leading_term(p) to select the greatest term in lexicographic order, with axis zero first. Select order="grlex" or order="grevlex" for graded orders, and pass axes= as a complete permutation when another axis should take priority. The query leaves the array's canonical storage order unchanged.

array_access_inspection.mojo Download
"""Inspect sparse support without changing the polynomial's storage order."""
from poly import sparse
from apn_mojo import Integer
from std.testing import assert_equal


def main() raises:
    var p = sparse.asarray([((2, 0, 0), 3), ((0, 3, 0), 5), ((0, 0, 0), 7)])
    assert_equal(sparse.leading_term(p)[0], (2, 0, 0))
    assert_equal(sparse.leading_term(p, order="grlex")[0], (0, 3, 0))
    assert_equal(sparse.leading_term(p, axes=List[Int]([1, 0, 2]))[1], Integer(5))
    assert_equal(sparse.variables(p), List[Int]([0, 1]))
    print("Lex selects 3*x^2; graded lex selects 5*y^3")
    print("Occurring axes: 0, 1")

Run from the repository root pixi run mojo run -I build docs/examples/array_access_inspection.mojo

Output

Lex selects 3*x^2; graded lex selects 5*y^3
Occurring axes: 0, 1

sparse.variables(p) returns sorted axis numbers that actually occur. The example uses a three-axis power shape, but only axes zero and one occur in its terms. Constants and zero return an empty list; zero has no leading term and raises DOMAIN for that query. Exact raw terms are combined before inspection, so canceled terms do not contribute to the result.

These queries also accept prepared numerical arrays without a context. For balls they describe stored terms: a coefficient containing both zero and nonzero values still contributes a term. Its presence does not prove that every polynomial in the family has the same leading term.

Find and remove common factors

array_exact_algebra.mojo Download
"""Recover a common factor and factor a polynomial from coefficient inputs."""
import poly
from std.testing import assert_equal


def main() raises:
    var p = poly.dense.multiply(poly.dense.power([-1, 1], 2), [2, 0, 2])
    var common = poly.dense.gcd(p, [-2, 2])
    print("GCD:", common.to_list())
    var bezout = poly.dense.extended_gcd(p, [-2, 2])
    var identity = poly.dense.add(poly.dense.multiply(bezout[1], p), poly.dense.multiply(bezout[2], [-2, 2]))
    assert_equal(identity.to_list(), bezout[0].to_list())
    print("Bezout identity checked")
    var factors = poly.dense.factor(p)
    print("Scalar:", factors[0], "Distinct factors:", len(factors[1]))
    var rebuilt = poly.dense.asarray([factors[0].copy()])
    for entry in factors[1]:
        rebuilt = poly.dense.multiply(rebuilt, poly.dense.power(entry[0], entry[1]))
    assert_equal(rebuilt.to_list(), p.to_list())
    print("Factor product checked")
    var division = poly.dense.pseudo_divmod([1, 0, 1], [1, 2])
    print("Scale:", division[0], "Quotient:", division[1].to_list(), "Remainder:", division[2].to_list())

Run from the repository root pixi run mojo run -I build docs/examples/array_exact_algebra.mojo

Output

GCD: [-2, 2]
Bezout identity checked
Scalar: 2 Distinct factors: 2
Factor product checked
Scale: 4 Quotient: [-1, 2] Remainder: [5]

The integer GCD includes coefficient content, so the example returns \(2(x-1)\). Over a field, gcd returns a monic polynomial. extended_gcd also returns multipliers: (g, u, v) satisfies \(up+vq=g\). It promotes integer inputs to QQ, where the nonzero GCD can always be made monic.

Choose division according to the result you need:

Operation Result Integer inputs
divmod(p, q) Quotient and remainder Promote to QQ
divide_exact(p, q) An optional exact quotient Stay in ZZ
pseudo_divmod(p, q) Scale, quotient, and remainder Stay in ZZ
divide_many(p, divisors) Quotients in divisor order, then a remainder Promote to QQ

For example, divide_exact([1, 1], [2]) returns None: \((1+x)/2\) has no integer coefficient representation. Use a rational coefficient when you want the quotient over QQ. Invalid inputs and work exhaustion still raise errors; they are not negative divisibility answers. Pseudo-division instead multiplies the dividend by \(\operatorname{lc}(q)^{\max(\deg p-\deg q+1,0)}\). The example gives \(4(x^2+1)=(2x-1)(2x+1)+5\).

Both namespaces provide content and primitive_part. Dense content is a scalar; sparse.content(p, axis=0) treats the other axes as coefficient variables and returns a polynomial in them. Multiplying content by primitive part reconstructs the input.

divide_many accepts a finite collection of polynomials of one Mojo input type. A list of prepared arrays works well; for nested coefficient lists, annotate the collection as List[List[Int]] or the corresponding scalar type. Sparse reduction defaults to lexicographic order, with axis zero first. Use order= and axes= to choose another reduction order. The remainder can depend on both monomial and divisor order; use the prepared Gröbner API when you need reduction by a Gröbner basis.

Factor or group repeated roots

factor returns (scalar, factors), with each factor stored alongside its positive multiplicity. The product in the example checks reconstruction. Integer factors are primitive with positive leading coefficient; rational factors are monic. Zero and constants return an empty factor list.

Use squarefree when you need multiplicity groups without splitting every irreducible factor. Its monic blocks are ordered by multiplicity, and integer inputs promote to QQ. A block can contain several irreducible factors.

Domain factor squarefree
ZZ Primitive integer factors Monic blocks over QQ
QQ, QQi Monic factors in the inferred field Monic blocks in that field
Prime field Monic factors with multiplicities Monic blocks, including inseparable inputs
AA Monic real algebraic factors; degree one or two for univariate inputs Monic blocks in AA
QQbar Monic complex algebraic factors; linear for univariate inputs Monic blocks in QQbar

Both operations accept dense univariate inputs and sparse multivariate inputs over every listed domain. The coefficient field matters: \(x^2+1\) stays irreducible over QQ, but splits into \((x-i)(x+i)\) over QQi. Choose exact complex coefficients to request that field.

array_factor_fields.mojo Download
"""Factor repeated finite-field and Gaussian-rational polynomials."""
import poly
from apn_mojo import ExactComplex
from std.testing import assert_equal, assert_true


def main() raises:
    # In GF(3), x^3 + 1 = (x + 1)^3, even though its derivative is zero.
    var finite = poly.dense.asarray([1, 0, 0, 1], modulus=3)
    var factors = poly.dense.factor(finite)
    assert_equal(factors[1][0][1], 3)
    print("GF(3) factor multiplicity:", factors[1][0][1])

    # ExactComplex coefficients select QQ(i), so x^2 + 1 splits.
    var complex_p = poly.coefficients((1, 0, ExactComplex(1)))
    var checked = poly.dense.factor_checked(complex_p)
    assert_true(poly.exact.check(checked))
    print("Gaussian-rational factors:", len(poly.exact.factors(checked)))

    var a = poly.sparse.asarray([((1, 0), 1), ((0, 1), 1)])
    var b = poly.sparse.asarray([((0, 1), 1), ((0, 0), 1)])
    var product = poly.sparse.multiply(poly.sparse.power(a, 2), poly.sparse.power(b, 3))
    var blocks = poly.sparse.squarefree_checked(product)
    assert_true(poly.exact.check(blocks))
    var values = poly.exact.factors(blocks)
    print("Sparse multiplicities:", values[0][1], values[1][1])

Run from the repository root pixi run mojo run -I build docs/examples/array_factor_fields.mojo

Output

GF(3) factor multiplicity: 3
Gaussian-rational factors: 2
Sparse multiplicities: 2 3

Use algebraic.asreal(1) or algebraic.ascomplex(1) in the coefficient sequence to choose AA or QQbar, even when the polynomial has rational coefficients. The algebraic factorization example shows how the field changes the factors and how exact.check verifies their irreducibility in these two domains.

Finite-field factorization extracts the leading scalar and handles repeated factors automatically. Its seed=0 default fixes the splitting sequence; other seeds change the work, while the sorted factors remain the same. The example also shows why a zero derivative need not mean a constant polynomial: in characteristic three, the derivative of \(x^3+1\) vanishes.

Sparse squarefree decomposition uses partial derivatives directly. General QQi, prime-field, AA and QQbar sparse factorization uses a dense substitution image, whose size grows with the product of the per-variable degree ranges, followed by a potentially exponential subset search. Set max_steps for large inputs; an exhausted budget raises LIMIT before the corresponding work or allocation. See the algorithm notes for the cost and correctness argument.

Sparse extended_gcd and pseudo_divmod still require a one-axis power shape and use dense conversion internally.

Recover coefficients from exact samples

array_exact_samples.mojo Download
"""Reuse exact sample nodes and change variables in a sparse polynomial."""
import poly
from std.testing import assert_equal


def main() raises:
    var plan = poly.dense.prepare_points([0, 1, 2], interpolation=True)
    var p = plan.interpolate([1, 4, 9])
    print("Recovered:", p.to_list())
    print("Samples:", plan.evaluate(p).to_list())
    var hermite = poly.dense.hermite_interpolate([0, 1], [[1, 2], [4, 4]])
    assert_equal(hermite.to_list(), p.to_list())
    print("Taylor coefficients:", poly.dense.jet_evaluate(p, 1, 4).to_list())
    print("Integral:", poly.dense.definite_integral([0, 1], 0, 1))
    var surface = poly.sparse.asarray([((2, 1), 1), ((0, 1), 1)])
    var x = poly.sparse.asarray([((1, 0), 1)])
    var y = poly.sparse.asarray([((0, 1), 1)])
    var swapped = poly.sparse.substitute(surface, [y.copy(), x.copy()])
    print("Swapped axes:", swapped.to_list())
    print("Integrated x:", poly.sparse.definite_integral(surface, 0, 1).to_list())

Run from the repository root pixi run mojo run -I build docs/examples/array_exact_samples.mojo

Output

Recovered: [1, 2, 1]
Samples: [1, 4, 9]
Taylor coefficients: [4, 4, 1, 0]
Integral: 1/2
Swapped axes: [((1, 0), 1), ((1, 2), 1)]
Integrated x: [((0, 1), 4/3)]

dense.interpolate(points, values) performs a single interpolation. prepare_points builds a product tree you can reuse for several polynomials; interpolation=True also caches interpolation weights. Samples keep their order and all zeros. Distinct nodes and matching sample counts determine a unique polynomial of degree below the node count.

The plan retains the field inferred from its nodes. Integer nodes select QQ. To work with exact complex coefficients, prepare the plan from nodes containing an ExactComplex, even if its imaginary part is zero. Modular nodes must share a prime. Copies of the plan share its immutable tree. Repeated nodes are allowed for evaluation when interpolation=False, but interpolation rejects them.

Hermite interpolation adds derivative constraints. Each inner sequence starts with the value, followed by consecutive derivatives. In the example, the two jets describe \((x+1)^2\) at zero and one. Set derivatives=False to supply Taylor coefficients instead. jet_evaluate returns those coefficients \(p^{(j)}(a)/j!\), including requested trailing zeros. It computes them without factorial division, which also makes it useful in finite fields.

Change variables and eliminate an axis

The sparse example swaps \(x\) and \(y\) simultaneously. Replacements keep the original variables, so replacing \(x\) with \(y\) and \(y\) with \(x\) turns \(x^2y+y\) into \(xy^2+x\). To replace only one axis, use substitute(p, [replacement], axes=[0]).

affine_transform(p, scale, shift) computes \(p(\text{scale}\,x+\text{shift})\); taylor_shift(p, point) computes \(p(x+\text{point})\). Sparse versions take axis=. definite_integral accepts exact scalar endpoints and promotes integer inputs to QQ; sparse integration leaves exponent zero on the integrated axis and preserves the full power shape.

For elimination, resultant(p, q) returns a scalar in the dense namespace. sparse.resultant(p, q, axis=0) returns a polynomial in the remaining axes, retaining the input shape. The sign follows the Sylvester determinant convention. Two nonzero constants in the selected variable raise DOMAIN. subresultants returns the indexed family \(S_0,\ldots,S_{m-1}\), where \(m\) is the smaller input degree in that variable; zero entries retain their indices.

Continue with checked results

You can retain and check exact algebra evidence directly through the checked array operations, including factor_checked. Roots, Gröbner bases, and CAD also have direct array workflows.

Every operation above accepts max_steps= or an explicit shared Budget. See the dense and sparse references for signatures and domain requirements.