Introduction

oCAS (Open Computer Algebra System) is a high-performance computer algebra system (CAS) written in Rust, released under the LGPL-3.0-or-later license. The project aims to match or exceed Symbolica and SageMath in core symbolic-computation performance, while providing multi-language interfaces through Python (PyO3) and C/C++ (cbindgen) bindings.

oCAS uses a layered crate architecture: 13 workspace members from the low-level arena allocator up to the top-level language bindings, with dependencies strictly pointing downward. All coefficient domains (arbitrary-precision integers/rationals, finite fields, real balls, complex numbers, double-precision floats) are abstracted behind a unified Domain trait, and the polynomial algebra, Gröbner basis, symbolic calculus, and ODE solving modules share the same expression infrastructure.


Version Feature Matrix

The table below lists the key features added in each oCAS release (0.1 → 0.27):

VersionDateKey additions
0.1.02026-06-29Workspace setup, arena allocator, unified error types, minimal C ABI example, cross-platform CI
0.2.02026-06-30Expression tree Atom (hash consing), parser based on logos/chumsky, normalizer
0.3.02026-06-30Coefficient domains (Integer/Rational/FiniteField/RealBall/Complex), dense/sparse polynomials, optional GMP/FLINT backends
0.4.02026-07-01Rewrite engine (wildcard patterns, AC matching, Rule/simplify), optional egg e-graph equality saturation
0.5.02026-07-01Symbolic differentiation diff, Taylor expansion taylor, heuristic integration integrate
0.6.02026-07-08Stabilized prelude API, rustdoc examples, proptest property tests, Criterion benchmarks, SymPy comparison tooling
0.7.02026-07-01Root isolation (Sturm's theorem), linear system solvers (ℚ/ℤ), Diophantine equations, Buchberger Gröbner bases
0.8.02026-07-02Evaluation engine (stack VM, CSE optimization, SIMD vectorization, Cranelift JIT backend)
0.9.02026-07-02Python bindings (PyO3 Expression/ExpressionEvaluator), C calculus API, C++ RAII wrappers, 33 pytest cases
0.10.02026-07-02Python Polynomial/Matrix classes, Bareiss determinant, FiniteField implements EuclideanDomain, mdBook documentation site
0.11.02026-07-03Correctness comparison framework (16 modules, 82 tests), complete ℤ/𝔽_p factorization (Cantor–Zassenhaus + Hensel), number theory primitives, multivariate GCD
0.12.02026-07-04RationalPolynomial rational function type, Brown PRS resultant, Karatsuba fast multiplication, partial fraction decomposition apart/together
0.13.02026-07-06F4 Gröbner basis algorithm (Gebauer–Moeller filtering, SimpCache, ℤ_p i64 fast path)
0.14.02026-07-18Risch symbolic integration (Hermite reduction, RDE, trigonometric/special functions), FGLM change of order, Hilbert bound
0.15.02026-07-20Multi-output JIT, f32 mixed precision, streaming evaluation (constant memory), arena reset + workspace pool, native i64 F4 pipeline
0.16.02026-07-21Arbitrary multivariate factorization (Wang EEZ), $n$-ary GCD (dense recursive evaluation–interpolation)
0.17.02026-07-22Algebraic number field factorization (Trager norm algorithm), $\mathrm{GF}(p^d)$ construction
0.18.02026-07-23Resource control Fuel, hyper-dual automatic differentiation HyperDual, Vegas Monte Carlo integration, tensor foundations (index slots/contraction/symmetrization)
0.19.02026-07-23F5 signature reduction algorithm (cyclic-6 ℤ₁₃ speedup ~1400× to 2.63s), unified Gröbner dispatch entry point, Rust 1.97 toolchain
0.20.02026-07-27ODE solvers (5 first-order + 2 second-order + power series), ODE classification engine classify_ode
0.20.12026-07-27Integrating factors, variation of parameters (VOP), extended method of undetermined coefficients (resonance/trig/superposition), reduction of order
0.21.02026-07-30BPSW primality testing, integer factorization (Pollard rho/p−1/p+1/ECM), discrete logarithms, modular polynomial GCD (Brown), 12 number theory Python bindings
0.22.02026-08-02McKay graph-isomorphism canonical labeling engine, tensor expression canonicalization, Young projectors, backtracking AC matching, multi-pattern replacement
0.23.02026-08-02Ideal operations (membership/sum/product/quotient/saturation/intersection), elimination orders, zero-dimensional solving, primary decomposition, Hilbert series, rational root theorem
0.24.02026-08-03Heuristic integration (LIATE integration by parts/trig substitution/Weierstrass), DoubleF64 double-precision floats (~31 significant digits)
0.25.02026-08-04Multi-modular Gröbner bases over ℚ (parallel lucky-prime F5, CRT + rational reconstruction, trace-free p-adic Hensel lift), parallel modular GCD, Algorithm::MultiModular with Auto routing
0.26.02026-08-04SWAR-packed monomial F5 fast path (u128), clone-free two-phase echelonization, grevlex benchmark variants; fixed the inverted degree direction in graded monomial orders (pre-existing wrong bases)
0.27.02026-09-06Symbolic-integration breadth: rule-table engine (families A–H), symbolic-constant rational backend, Weierstrass linear arguments, bounded expansion retry, trig product-to-sum; Rubi 1892-problem coverage 5.87% → 9.62%
0.27.12026-09-10Integration-breadth mechanism push: Chebyshev binomials, trig-denominator power reductions, exp-log kernel substitutions, general sqrt-quadratic engine + Euler III, inverse-trig kernels, single-trig-kernel rational forms; coverage 9.62% → 16.44%; two wrong-answer classes fixed
0.27.22026-09-11Verified-coverage criterion (independent numeric oracle: 5-point central differences + elliptic defining integrals), stage tracing + all 33 hang attributions, deterministic per-stage budgets, bounded-expansion pre-pass, hyperbolic closed forms, rational-derivative kernel substitution, trig phase shift, inverse-composition cancellation, exp(inverse function) algebraization, half-power front-end + elliptic Legendre reduction (EllipticF/E); six wrong-answer classes fixed (verified coverage 93.1%, mismatches 0, timeouts 33 → 13, wall clock −22%)
0.27.32026-09-120.27-line close-out: special-function derivative table + oracle heads (Ei/Ei(n,z)/Si/Ci/Shi/Chi/Fresnel), special-function reduction families (polynomial × F, Ei(n,·), F/xᵐ, F²), exact linear-square fold p²+2pq+q² → (p+q)², affine-argument half-power front-end with the sheet factor on both branches; Rubi coverage 18.45% → 19.56% (+21 solved, 0 regressed, mismatches 0, timeouts 13 → 12)
0.28.02026-09-13Integration-mechanism correctness foundation: symbolic certificate engine (structural/elementary-field layers with I² = −1 reduction and dependent-atom merging, deterministic budgets), three-valued Outcome and integrate_outcome (exposed to Python/C), expression-level cycle detection (budget-aware entry keys plus a separate residue budget), regular-tower dependency merging, in-tower exact verification of Risch results (caught and contained a latent wrong answer), and a hyperbolic front-end disabled for its wall-clock cost; Rubi coverage 19.56% → 19.61% (371 solved, +1 newly solved / 0 regressed), mismatches 0, timeouts flat at 12, wall clock −6.0%; certified_rate = 28.8% (target 1.0 not met, recorded honestly)

Module Architecture

oCAS consists of 13 crates with the following dependency graph (arrow A → B means A depends on B; ocas-py/ocas-c also depend on ocas-eval, ocas-rewrite and other core crates — only representative edges are shown; dashed edges are optional dependencies):

graph TD
    CORE["ocas-core<br/>Arena, Fuel, thread pool"]
    DOMAIN["ocas-domain<br/>coefficient domains, number theory, algebraic numbers"]
    POLY["ocas-poly<br/>polynomials, matrices, Gröbner bases"]
    ATOM["ocas-atom<br/>expression tree Atom, tensors"]
    PARSE["ocas-parse<br/>parser"]
    REWRITE["ocas-rewrite<br/>pattern matching, simplification"]
    CALC["ocas-calc<br/>calculus, ODE, integration"]
    EVAL["ocas-eval<br/>evaluation, JIT, numeric integration"]
    GPL["ocas-gpl<br/>GPL backend isolation"]
    TOP["ocas<br/>top-level crate + prelude"]
    PY["ocas-py<br/>Python bindings (PyO3)"]
    C["ocas-c<br/>C/C++ bindings (cbindgen)"]
    TESTS["ocas-tests<br/>correctness comparisons, pytest"]

    DOMAIN --> CORE
    ATOM --> CORE
    EVAL --> CORE
    EVAL --> DOMAIN
    EVAL --> ATOM
    PARSE --> ATOM
    REWRITE --> ATOM
    REWRITE --> PARSE
    CALC --> ATOM
    CALC --> DOMAIN
    CALC --> POLY
    CALC --> REWRITE
    POLY --> DOMAIN
    TOP --> DOMAIN
    TOP --> ATOM
    TOP --> POLY
    TOP --> CALC
    TOP --> EVAL
    TOP --> REWRITE
    TOP --> PARSE
    TOP --> CORE
    TESTS --> TOP
    TOP -.-> PY
    TOP -.-> GPL
    PY --> CALC
    C --> CALC

    style CORE fill:#e8f4fd,stroke:#2196F3
    style DOMAIN fill:#e8f4fd,stroke:#2196F3
    style POLY fill:#fff3e0,stroke:#FF9800
    style ATOM fill:#e8f5e9,stroke:#4CAF50
    style CALC fill:#fce4ec,stroke:#E91E63
    style EVAL fill:#f3e5f5,stroke:#9C27B0
    style TOP fill:#fffde7,stroke:#FDD835
    style PY fill:#efebe9,stroke:#795548
    style C fill:#efebe9,stroke:#795548

Crate overview:

crateResponsibility
ocas-coreArena allocator, Fuel resource control, ThreadPool, GMP type aliases
ocas-domainCoefficient domain traits (Domain/EuclideanDomain), Integer/Rational/FiniteField/RealBall/Complex/DoubleF64/AlgebraicExtension, number theory functions, assumption system
ocas-polyDense univariate/sparse multivariate polynomials, RationalPolynomial, matrices, GCD, factorization, Gröbner bases (Buchberger/F4/F5), FGLM, ideal operations
ocas-atomAtom/Arena/Symbol expression tree, hash consing, tensors (graph canonicalization/Young projectors)
ocas-parseExpression parser based on logos/chumsky
ocas-rewriteWildcard pattern matching (AC backtracking), Rule/simplify/transform, optional egg e-graph
ocas-calcSymbolic differentiation diff, layered integration pipeline (rational → Risch → trigonometric → special functions → heuristic), Taylor expansion, ODE solvers (first/second-order, series, Laplace, systems), partial fractions
ocas-evalStack VM interpreter, SIMD vectorized evaluation, Cranelift JIT, streaming evaluation, Vegas Monte Carlo numeric integration
ocasTop-level re-export crate + prelude::*
ocas-pyPyO3 Python bindings (25 classes + 32 functions)
ocas-cC/C++ FFI bindings (91 #[no_mangle] exports) + ocas.hpp RAII wrappers
ocas-gplGPL-licensed backend isolation (GMP/MPFR/FLINT)
ocas-testsCorrectness comparison framework (SymPy/SageMath/Symbolica), pytest suite, C API integration tests

Key Features

  • 13 crates — layered Rust architecture from the arena runtime to language bindings, with dependencies strictly downward.
  • 7 coefficient domains — arbitrary-precision integers/rationals, finite fields $\mathbb{F}_p$, real balls, complex numbers, double-precision floats (DoubleF64 ~31 digits), algebraic number fields $\mathbb{Q}(\alpha)$ / $\mathrm{GF}(p^d)$.
  • Polynomial algebra — dense univariate/sparse multivariate polynomials, Karatsuba multiplication, GCD (Brown's modular GCD), multivariate GCD, factorization (Cantor–Zassenhaus + Hensel + Wang EEZ), algebraic number field factorization (Trager).
  • Gröbner bases — three algorithms (Buchberger / F4 / F5), FGLM change of order, ideal operations (membership/sum/product/quotient/saturation/intersection/elimination/radical/primary decomposition), Hilbert series.
  • Symbolic calculus — differentiation, Taylor expansion, layered integration pipeline (rational → Risch → trigonometric → special functions → heuristic → unevaluated form).
  • ODE solving — first-order (separable/linear/Bernoulli/exact/homogeneous/integrating factors), second-order (constant coefficient/Cauchy–Euler/reduction of order/undetermined coefficients/variation of parameters), power-series and Frobenius solutions, Laplace IVP, 2×2 systems.
  • Evaluation engine — stack VM interpreter, SIMD batch evaluation (f64x4), Cranelift JIT (f64/f32), streaming evaluation (constant memory).
  • Automatic differentiation — forward mode with hyper-dual numbers HyperDual<T>, first- and higher-order partial derivatives.
  • Tensor algebra — index slots, explicit contraction, McKay graph-isomorphism canonicalization (1-WL refinement + path invariant pruning), Young projectors.
  • Numeric integration — adaptive Monte Carlo (Vegas), supporting high-dimensional definite integrals.
  • Number theory — BPSW primality testing, integer factorization (trial division/Pollard rho/p−1/p+1/ECM), discrete logarithms (BSGS + Pohlig–Hellman), Euler φ / Möbius / divisor functions.
  • Rewriting and simplification — AC backtracking pattern matching, wildcards (x__/x___), fixpoint simplification, fuel-limited simplification, optional egg e-graph equality saturation.
  • Three-language bindings — Rust prelude API, Python (PyO3, 25 classes + 32 functions), C/C++ (cbindgen, 91 exports + RAII wrappers).
  • Correctness framework — cross-validation against SymPy, SageMath, and Symbolica across multiple mathematical modules (1550 #[test]/#[tokio::test] annotations as of 0.28.0).
  • Optional numeric backends — GMP/MPFR/FLINT behind feature flags, with GPL backends isolated in ocas-gpl.

Quick Navigation

TopicDescriptionEntry point
Rust API referencePer-item documentation of all public types and functionsAPI reference overview
Python API referencePyO3 binding signatures, parameters, exceptions, examplesPython API
C/C++ API referenceFFI prototypes, error codes, memory management, examplesC/C++ API
Mathematical foundationsProgressive exposition from polynomial algebra to the Risch algorithmMath overview
Algorithm detailsImplementation details of Gröbner bases, factorization, number theory, integration, ODEsAlgorithms
SolversLinear/Diophantine/polynomial-system/ODE solversSolvers
Rewriting and simplificationPattern matching, rule-based simplification, e-graphsRewriting
Evaluation and JITVM interpreter, SIMD, Cranelift JITEvaluation
Numeric integrationVegas adaptive Monte CarloNumeric integration
Automatic differentiationForward-mode AD with hyper-dual numbersAutodiff
TensorsIndices, contraction, canonicalization, Young projectorsTensors
Benchmarks and performanceCriterion benchmarks, SymPy/SageMath comparisonsPerformance
CorrectnessCross-system validation testsCorrectness
Build guideBackend selection, Windows build, contributingBuild

Project Status

oCAS is currently at version 0.28.0. The core symbolic engine, polynomial algebra, Gröbner bases and algebraic geometry, ODE solving, JIT evaluation, three-language bindings, and the correctness comparison framework are feature-complete. The roadmap to a stable 1.0 can be found in the roadmap.

Getting Started

This chapter gets you up and running with oCAS in about 5 minutes. oCAS 0.24 supports three language interfaces: Rust, Python, and C/C++.

System requirements

LanguageMinimum versionNotes
Rust1.97+ (edition 2024)Install via rustup recommended
Python3.10+Use pip install or build from source
C/C++C11 / C++17Building the C library requires cargo

Rust

Installation

Add oCAS to your Cargo.toml:

[dependencies]
ocas = "0.24"

To enable optional backends, turn on features:

[dependencies]
ocas = { version = "0.24", features = ["gmp", "jit"] }

Commonly used features:

FeatureDescription
gmpUse GMP arbitrary-precision arithmetic (instead of pure-Rust num-bigint)
mpfrUse MPFR ball arithmetic (real interval arithmetic)
jitCranelift JIT-compiled evaluator
simdSIMD-vectorized batch evaluation
egge-graph equality saturation engine
nttNTT fast polynomial multiplication (𝔽_p)
mimallocUse the mimalloc allocator
system-libsLink against system-installed GMP/MPFR (required on Windows/MinGW)

See the Rust API reference for the complete feature list.

Verify the Installation

Create a minimal project to confirm everything works:

cargo new ocas-test && cd ocas-test

Add the dependency to Cargo.toml:

[dependencies]
ocas = "0.24"

Write the following to src/main.rs:

use ocas::prelude::*;

fn main() {
    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);
    let expr = parse(&ctx, "x^2 + 2*x + 1").unwrap();
    let d = diff(&ctx, expr, Symbol::new("x"));
    println!("d/dx(x² + 2x + 1) = {}", d);
    // Output: d/dx(x² + 2x + 1) = 2 + 2*x
}

Run it:

cargo run

If it prints 2 + 2*x, the installation succeeded.

Basic Usage

Symbolic Differentiation and Simplification

use ocas::prelude::*;
use ocas::ocas_rewrite::rules::default_rules;

fn main() {
    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);

    // Parse the expression
    let expr = parse(&ctx, "x^2").unwrap();

    // Differentiate
    let d = diff(&ctx, expr, Symbol::new("x"));
    println!("derivative: {}", d);
    // Output: 2*x

    // Simplify (default rule set: identity removal, constant folding)
    let e2 = parse(&ctx, "x + 0 + y*0 + z*1").unwrap();
    let rules = default_rules(&ctx, &());
    let s = simplify(&ctx, e2, &rules, 100);
    println!("simplified: {}", s);
    // Output: x + z
}

Polynomial Arithmetic

use ocas::prelude::*;
use ocas_poly::dense::DenseUnivariatePolynomial;
use ocas_domain::rational::RationalDomain;
use ocas_domain::Rational;

fn main() {
    let domain = RationalDomain;
    // 1 + 2x + x²
    let p = DenseUnivariatePolynomial::from_coeffs(
        domain,
        vec![Rational::new(1, 1), Rational::new(2, 1), Rational::new(1, 1)],
    );
    // 1 + x
    let q = DenseUnivariatePolynomial::from_coeffs(
        domain,
        vec![Rational::new(1, 1), Rational::new(1, 1)],
    );

    let (quot, rem) = p.div_rem(&q).unwrap();
    // DenseUnivariatePolynomial does not implement Display; assert coefficients
    assert_eq!(quot.coeffs(), &[Rational::new(1, 1), Rational::new(1, 1)]); // 1 + x
    assert_eq!(rem.coeffs(), &[]); // remainder 0
}

Solving Linear Systems

use ocas_calc::solve::solve_linear_rational;

fn main() {
    // Solve 2x + y = 5, x - y = 1
    let a = vec![vec![2, 1], vec![1, -1]];
    let b = vec![5, 1];

    let solution = solve_linear_rational(&a, &b).unwrap();
    println!("x = {}/{}, y = {}/{}",
        solution[0].0, solution[0].1,
        solution[1].0, solution[1].1);
    // Output: x = 2/1, y = 1/1
}

Number Theory

use ocas_domain::number_theory::{
    is_prime, factor::factor_integer, functions::euler_phi,
};

fn main() {
    println!("is_prime(97) = {}", is_prime(&97.into()));
    // Output: is_prime(97) = true

    let factors = factor_integer(&360.into());
    println!("360 = {}", factors.iter()
        .map(|(p, e)| format!("{}^{}", p, e))
        .collect::<Vec<_>>()
        .join(" × "));
    // Output: 360 = 2^3 × 3^2 × 5^1

    println!("φ(360) = {}", euler_phi(&360.into()));
    // Output: φ(360) = 96
}

For more Rust APIs, see:


Python

Installation

pip install ocas

Requires Python ≥ 3.10 (uses the PyO3 abi3 stable ABI).

Verify the Installation

python -c "import ocas; e = ocas.Expression('x^2'); print(e.diff('x'))"

If it prints 2*x, the installation succeeded. To verify in an interactive Python session:

>>> import ocas
>>> e = ocas.Expression("x^2 + 2*x + 1")
>>> e.diff("x")
2 + 2*x
>>> e.simplify()
1 + 2*x + x^2

Basic Usage

Symbolic Differentiation and Simplification

import ocas

e = ocas.Expression("x^2")

# Differentiate
d = e.diff("x")
print(f"derivative: {d}")
# Output: derivative: 2*x

# Simplify (default rule set: identity removal, constant folding)
e2 = ocas.Expression("x + 0 + y*0 + z*1")
s = e2.simplify()
print(f"simplified: {s}")
# Output: simplified: x + z

Polynomial Arithmetic

import ocas

# 1 + 2x + x²
p = ocas.Polynomial([1, 2, 1])
print(f"degree: {p.degree()}")         # degree: 2
print(f"p(2) = {p.eval(2)}")         # p(2) = 9

# Polynomial GCD
q = ocas.Polynomial([1, 1])          # 1 + x
g = p.gcd(q)
print(f"gcd = {g}")                   # gcd = 1 + x

# Factorization
factors = p.factor()
print(f"factors: {factors}")         # factors: [(1 + x, 2)]

Finite Field Arithmetic

import ocas

gf5 = ocas.FiniteField(5)
q = ocas.Polynomial([1, 2, 1], domain=gf5)
print(q.eval(3))  # (1 + 6 + 9) mod 5 = 16 mod 5 = 1

Matrix Operations

import ocas

m = ocas.Matrix([[1, 2], [3, 4]])
print(f"determinant: {m.determinant()}")       # determinant: -2
print(f"transpose: {(m.transpose()).rows()}")   # transpose: [[1, 3], [2, 4]]
print(f"m × m: {(m @ m).rows()}")          # m × m: [[7, 10], [15, 22]]

Number Theory

import ocas

print(ocas.isprime(97))               # True
print(ocas.factorint(360))             # [("2", 3), ("3", 2), ("5", 1)]
print(ocas.totient(360))               # 96
print(ocas.nextprime(100))             # 101
print(ocas.mobius(30))                 # -1
print(ocas.divisor_count(360))         # 24

ODE Solving

import ocas

e = ocas.Expression("Derivative(y(x), x) + y(x)")  # y' + y = 0

# Classify the available solving methods
methods = ocas.classify_ode(e, "y", "x")
print(methods)

# Solve y' + y = 0
sol = ocas.dsolve(e, "y", "x")
print(sol)  # y = C1*exp(-x)

For more Python APIs, see the Python API reference.


C/C++

Building the C Library

oCAS's C library is built with cargo. Clone the source repository and compile:

git clone https://github.com/charleshzh/ocas.git
cd ocas
cargo build -p ocas-c --release

The build artifacts are placed in target/release/:

FileDescription
libocas_c.so (Linux) / ocas_c.dll (Windows) / libocas_c.dylib (macOS)Dynamic library
libocas_c.a (Linux/macOS) / ocas_c.lib (Windows)Static library

The header file ocas.h lives in the ocas-c/include/ directory. C++ users can also use ocas.hpp in the same directory (RAII wrappers covering the expression, number theory, and tensor domains).

Windows users: the MSYS2 MINGW64 environment is recommended. See Building on Windows.

Verify the Installation

Create a minimal test file test_ocas.c:

#include <stdio.h>
#include "ocas.h"

int main(void) {
    struct ocas_OcasExpr *e = ocas_expr_parse("x^2 + 2*x + 1", NULL);
    if (!e) {
        fprintf(stderr, "parse failed\n");
        return 1;
    }

    struct ocas_OcasExpr *d = ocas_expr_diff(e, "x", NULL);
    char *s = ocas_expr_to_string(d, NULL);
    printf("d/dx(x² + 2x + 1) = %s\n", s);
    /* Output: d/dx(x² + 2x + 1) = 2 + 2*x */

    ocas_string_free(s);
    ocas_expr_free(d);
    ocas_expr_free(e);
    return 0;
}

Compile and run (Linux example; adjust the paths to match your environment):

# assuming oCAS is built at /path/to/ocas
gcc test_ocas.c -I/path/to/ocas/ocas-c/include \
    -L/path/to/ocas/target/release -locas_c -lm -ldl -lpthread -o test_ocas
LD_LIBRARY_PATH=/path/to/ocas/target/release ./test_ocas

If it prints 2 + 2*x, the installation succeeded.

Basic Usage

Symbolic Differentiation

#include <stdio.h>
#include "ocas.h"

int main(void) {
    struct ocas_OcasExpr *e = ocas_expr_parse("x^2", NULL);
    struct ocas_OcasExpr *d = ocas_expr_diff(e, "x", NULL);

    char *s = ocas_expr_to_string(d, NULL);
    printf("derivative: %s\n", s);
    /* Output: derivative: 2*x */

    ocas_string_free(s);
    ocas_expr_free(d);
    ocas_expr_free(e);
    return 0;
}

Number Theory

#include <stdio.h>
#include "ocas.h"

int main(void) {
    char *result;

    /* Primality test */
    int prime = ocas_ntheory_isprime("97", NULL);
    printf("is_prime(97) = %s\n", prime ? "true" : "false");
    /* Output: is_prime(97) = true */

    /* Integer factorization */
    result = ocas_ntheory_factorint("360", NULL);
    printf("360 = %s\n", result);
    /* Output: 360 = 2:3,3:2,5:1 */
    ocas_string_free(result);

    /* Euler φ function */
    result = ocas_ntheory_totient("360", NULL);
    printf("φ(360) = %s\n", result);
    /* Output: φ(360) = 96 */
    ocas_string_free(result);

    return 0;
}

C++ RAII Wrappers

If you use C++, the RAII wrappers in ocas.hpp save you from manual memory management:

#include <iostream>
#include "ocas.h"
#include "ocas.hpp"

int main() {
    ocas::Expression e("x^2 + 2*x + 1");
    auto d = e.diff("x");
    std::cout << "d/dx = " << d.to_string() << std::endl;
    // Output: d/dx = 2 + 2*x

    // Number theory (no manual free)
    std::cout << "is_prime(97) = " << ocas::ntheory::isprime("97") << std::endl;
    std::cout << "360 = " << ocas::ntheory::factorint("360") << std::endl;
    std::cout << "φ(360) = " << ocas::ntheory::totient("360") << std::endl;

    return 0;
}

For more C/C++ APIs, see the C/C++ API reference.


FAQ

Rust

Q: Compilation is slow, what can I do?

oCAS contains a lot of generic code, so the first build can be slow. Suggestions:

  • During development use cargo build (dev profile, dependencies already use opt-level = 3)
  • For releases use cargo build --release (enables LTO)
  • Use sccache or cargo-nextest to speed up incremental builds

Q: The gmp feature fails because the GMP library is not found?

On Linux/macOS, make sure the GMP development package is installed:

# Ubuntu/Debian
sudo apt install libgmp-dev

# macOS (Homebrew)
brew install gmp

On Windows (MSYS2 MINGW64):

pacman -S mingw-w64-x86_64-gmp

Or use the system-libs feature to link against system-installed libraries. See Building on Windows.

Q: The flint feature is unavailable on Windows?

The upstream flint3-sys crate only supports POSIX platforms, so the flint feature is not supported on Windows for now.

Python

Q: pip install ocas fails?

Check that Python ≥ 3.10 is used:

python --version

To build from source:

pip install maturin
maturin develop --release -m ocas-py/Cargo.toml

Q: Why do some functions in the Python bindings have a py_ prefix?

Some Gröbner functions (e.g. ocas.py_groebner_basis) keep the py_ prefix because the PyO3 bindings have not yet added #[pyo3(name=...)] annotations. Just call the functions with the prefixed names.

C/C++

Q: Linking fails with undefined reference?

Make sure the link order is correct — libocas_c goes after your source file:

gcc test.c -locas_c -lm -ldl -lpthread

And make sure LD_LIBRARY_PATH (Linux) or PATH (Windows) contains the library directory.

Q: Some functions are missing from ocas.h?

The ocas.h shipped in the repository may not be up to date. cbindgen generates a fresh header under target/release/build/ocas-c-*/out/ at build time. Use that file instead.

Q: What functionality do the C++ wrappers in ocas.hpp cover?

ocas.hpp currently covers three domains: expressions (ocas::Expression), number theory (ocas::ntheory::*), and tensors (ocas::tensor::*). For the remaining domains (polynomials, Gröbner, ODEs, etc.) use the C API directly.

General

Q: Numeric results are imprecise?

By default oCAS uses pure-Rust num-bigint rational arithmetic, which is exact. If you need high-precision floats (interval arithmetic), enable the mpfr feature:

ocas = { version = "0.24", features = ["mpfr"] }

Q: How do I check the oCAS version?

cargo metadata --format-version=1 | jq '.packages[] | select(.name=="ocas") | .version'

Next Steps

Architecture

oCAS is organized as a Cargo workspace of 12 public crates (plus ocas-tests for integration tests and benchmarks) with strict downward layering. Each crate may only depend on lower-level crates; no reverse or cyclic dependencies are permitted.

LevelcrateResponsibility
5 Bindingsocas, ocas-py, ocas-cRust, Python, C/C++ public API
4 Applicationocas-calc, ocas-eval, ocas-parseCalculus, evaluation, parsing
3 Symbol engineocas-atom, ocas-rewriteAtom, converters, pattern matching, e-graph
2 Algebra kernelocas-domain, ocas-polyDomains, polynomials, number theory
1 Numerical backendocas-coreGMP encapsulation (the FLINT backend lives in ocas-poly)
0 Runtimeocas-coreArena, errors, thread pool, FFI

Notable modules

ocas-poly::factor — Polynomial factorization using Hensel lifting and finite-field techniques. Supports square-free decomposition and full factorization over the integers and rationals.

ocas-poly::groebner — Gröbner basis computation with three algorithms: Buchberger, F4 (production default), and F5 (signature-based). Supports configurable monomial orders (Lex, Grevlex, WeightOrder, BlockOrder) and FGLM conversion for zero-dimensional ideals.

ocas-poly::roots — Real root isolation for univariate polynomials.

ocas-tests::correctness — Cross-validation framework with 201 #[test] tests across 19 mathematical modules (as of 0.24.0), comparing oCAS results against SymPy, SageMath, and Symbolica.

See ARCHITECTURE_EN.md in the repository for the full design document.

Rust API Reference

oCAS's Rust API uses a layered crate architecture. The top-level ocas crate re-exports all commonly used types and functions through its prelude module, so a single import is all you need:

#![allow(unused)]
fn main() {
use ocas::prelude::*;
}

Module Hierarchy

graph TD
    OC["ocas<br/>top-level entry point + prelude"]
    CORE["ocas-core<br/>Arena, errors, fuel"]
    DOMAIN["ocas-domain<br/>coefficient domains, number theory, algebraic number fields"]
    ATOM["ocas-atom<br/>expression trees, tensors"]
    PARSE["ocas-parse<br/>string parsing"]
    POLY["ocas-poly<br/>polynomials, matrices, Gröbner bases"]
    REWRITE["ocas-rewrite<br/>pattern matching, simplification, rewriting"]
    CALC["ocas-calc<br/>calculus, ODEs, solvers"]
    EVAL["ocas-eval<br/>evaluator, JIT, numeric integration"]

    OC --> CORE
    OC --> DOMAIN
    OC --> ATOM
    OC --> PARSE
    OC --> POLY
    OC --> REWRITE
    OC --> CALC
    OC --> EVAL

    ATOM --> CORE
    ATOM --> DOMAIN
    PARSE --> CORE
    PARSE --> ATOM
    POLY --> CORE
    POLY --> DOMAIN
    REWRITE --> CORE
    REWRITE --> ATOM
    REWRITE --> PARSE
    CALC --> CORE
    CALC --> DOMAIN
    CALC --> ATOM
    CALC --> POLY
    CALC --> REWRITE
    EVAL --> CORE
    EVAL --> ATOM
    EVAL --> DOMAIN

    style OC fill:#4a9eff,stroke:#2d7ad4,color:#fff
    style CORE fill:#6c757d,stroke:#545b62,color:#fff
    style DOMAIN fill:#28a745,stroke:#1e7e34,color:#fff
    style ATOM fill:#17a2b8,stroke:#117a8b,color:#fff
    style PARSE fill:#6f42c1,stroke:#59359a,color:#fff
    style POLY fill:#fd7e14,stroke:#ca6510,color:#fff
    style REWRITE fill:#e83e8c,stroke:#b82d6e,color:#fff
    style CALC fill:#20c997,stroke:#17a085,color:#fff
    style EVAL fill:#ffc107,stroke:#d39e00,color:#333
CrateResponsibility
ocas-coreArena bump allocator, unified error type OcasError, fuel counter Fuel
ocas-domainCoefficient-domain traits (Domain, EuclideanDomain) and implementations (integers, rationals, finite fields, real balls, complex numbers, double-precision floats, algebraic number fields), number-theory function library, hyper-dual automatic differentiation
ocas-atomExpression tree (Atom, AtomNode, AtomArena), hash-consing, tensor system
ocas-parseString → Atom parser
ocas-polyPolynomials (dense univariate, sparse multivariate, rational function fields), matrices, Gröbner-basis algorithms (Buchberger/F4/F5), ideal operations, FGLM order conversion
ocas-rewritePattern matching (AC matching), simplification (fixed-point iteration), bottom-up transformation, E-graph (optional)
ocas-calcSymbolic differentiation, integration (Risch pipeline), Taylor expansion, ODE solving, Diophantine/linear/polynomial-system solvers
ocas-evalStack-VM interpreter, JIT compilation (Cranelift), SIMD batch evaluation, Vegas Monte Carlo numeric integration

Prelude Contents

use ocas::prelude::* imports all of the following items. The most commonly used types can also be accessed directly as ocas::TypeName.

Expression Trees

NameKindDescription
AtomstructCopy handle pointing to an expression node in the arena
AtomArenastructHash-consing constructor that builds expressions from an Arena
AtomNodeenumNode variants: Num/Var/Fun/Add/Mul/Pow
SymbolstructInterned string identifier, Copy
normalizefnNormalizes an expression (sorting, merging like terms)
parsefnParses a string into an Atom
ParseErrorenumParse error

Coefficient Domains

NameKindDescription
DomaintraitBasic coefficient-domain arithmetic (zero/one/add/mul/sub/neg, etc.)
EuclideanDomaintraitEuclidean domain (+ div_rem/gcd/lcm)
IntegerstructArbitrary-precision integer
IntegerDomainstructInteger domain implementation
RationalstructArbitrary-precision rational number
RationalDomainstructRational number field implementation
FiniteFieldstruct𝔽ₚ finite field
FiniteFieldElementstructFinite-field element
RealBallstructReal ball arithmetic (requires the mpfr feature)
RealBallDomainstructReal ball domain implementation
ComplexstructComplex number
ComplexDomainstructComplex number field implementation
DoubleF64structDouble-precision float (~31 significant digits)
DoubleF64DomainstructDouble-precision float domain implementation
AlgebraicExtensionstructAlgebraic number field ℚ(α) or GF(p^d)
AlgebraicElementstructAlgebraic number field element
AlgebraicNumberFieldstructAlgebraic number field constructor
AssumptionenumAssumption variants (Positive/Negative/Integer/Real/Complex, and 8 more)
AssumptionsstructAssumption set
SymbolAssumptionsstructPer-symbol assumption management

Polynomials

NameKindDescription
DenseUnivariatePolynomialstructDense univariate polynomial
SparseMultivariatePolynomialstructSparse multivariate polynomial
RationalPolynomialstructElement p/q of a rational function field
RootIntervalstructAn interval containing exactly one real root (low/high bounds)
MonomialOrdertraitMonomial order trait
LexstructLexicographic order
GrlexstructGraded lexicographic order
GrevlexstructGraded reverse lexicographic order
WeightOrderstructWeighted order
BlockOrderstructBlock elimination order
SubOrderstructSub-order
monomial_dividesfnMonomial divisibility test
monomial_lcmfnMonomial least common multiple
monomial_are_coprimefnMonomial coprimality test

Matrices

NameKindDescription
MatrixstructMatrix over a domain (Bareiss determinant, Gaussian-elimination solving)
MatrixErrorenumMatrix operation error

Gröbner Bases

NameKindDescription
GroebnerBasisstructGröbner-basis result (polynomial list plus metadata)
buchbergerfnBuchberger algorithm entry point
f4fnF4 algorithm entry point (matrix row-echelon batching)

Calculus

NameKindDescription
difffnSymbolic differentiation
integratefnSymbolic integration (layered pipeline)
integrate_outcomefnCertified integration: three-valued outcome + symbolic certificate (0.28.0)
integrate_outcome_with_optionsfnCertified integration with options (0.28.0)
integrate_heuristicfnHeuristic integration (without Risch)
integrate_with_fuelfnIntegration with a fuel limit
taylorfnTaylor expansion
substitutefnExpression substitution
apartfnPartial-fraction decomposition

Solvers

NameKindDescription
solve_linear_rationalfnLinear system Ax=b over ℚ
solve_linear_integerfnLinear system Ax=b over ℤ
solve_diophantinefnDiophantine equation ax+by=c
DiophantineSolutionstructDiophantine solution (particular solution + steps)
SolveErrorenumSolve error
solve_polynomial_systemfnPolynomial system solving (zero-dimensional / positive-dimensional / empty)
classify_odefnODE type classification
dsolvefnSymbolic ODE solving
dsolve_ivpfnODE initial-value problem (Laplace)
dsolve_systemfn2×2 ODE system solving
ODEstructODE description
ODESolutionenumODE solution (explicit/implicit/parametric/series/system/unsolved)
ODETypeenumODE type enumeration

Rewriting & Simplification

NameKindDescription
PatternenumMatch pattern (Literal/Wildcard/Add/Mul/Pow/Fun)
RulestructRewrite rule
BindingsstructPattern binding results
MatchErrorenumMatch error (incl. BudgetExhausted)
WildcardLevelenumWildcard level (Single/Sequence/NullSequence)
match_patternfnAC pattern matching
simplifyfnFixed-point simplification
simplify_with_fuelfnSimplification with a fuel limit
transformfnBottom-up transformation

Evaluation & JIT

NameKindDescription
ExpressionEvaluatorstructStack-VM interpreter
FunctionMapstructCustom function registry
EvaluationDomaintraitEvaluation-domain constraint
EvaluationErrorenumEvaluation error
EvalTreestructEvaluation tree
InstrenumInstruction enum
InstructionstructInstruction details
SlotenumOperand slot (Param/Const/Temp)
PowfExtensiontraitFloating-point power extension
VectorEvaluatorstructSIMD batch evaluation (requires the simd feature)
JitEnginestructJIT compilation engine (requires the jit feature)
JitCompiledFunctionstructJIT compilation result

Numeric Integration

NameKindDescription
VegasstructVegas Monte Carlo integrator
VegasOptionsstructVegas options (bin count, sample count, iterations, etc.)
IntegrateResultstructIntegration result (value + error)
IntegratorstructIntegrator interface
StatisticsAccumulatorstructInverse-variance weighted accumulator
integrate_1dfn1-D numeric integration convenience function

Automatic Differentiation

NameKindDescription
DualShapestructDerivative layout description
HyperDualstructHyper-dual number
DualCoefftraitDual coefficient constraint
new_first_orderfnFirst-order dual convenience constructor

Tensors

NameKindDescription
TensorstructNamed-index tensor
IndexSlotstructIndex slot (label + position)
IndexPositionenumUpper (contravariant) / Lower (covariant)
SymmetryenumNone/Symmetric/Antisymmetric
ContractedenumContraction result (Scalar/Product)
TensorProductstructTensor product
contractfnIndex contraction
symmetrise_signfnAntisymmetrization sign

Runtime

NameKindDescription
ArenastructBump allocator
OcasErrorenumUnified error type
ResulttypeResult<T, OcasError>
FuelstructFuel counter (prevents infinite loops)

Feature Flags Quick Reference

Enable optional backends and acceleration features via the features key in Cargo.toml:

[dependencies]
ocas = { version = "0.24", features = ["gmp", "jit"] }
FeatureDefaultDescriptionSub-crate features involved
gmpnoUse GMP arbitrary-precision arithmetic (significantly speeds up big-integer operations)ocas-domain/gmp, ocas-poly/gmp, ocas-core/gmp
mpfrnoEnable MPFR real ball arithmetic (the RealBall type)ocas-domain/mpfr
flintnoUse the FLINT number-theory library to accelerate polynomial operationsocas-poly/flint
pythonnoEnable Python bindings (PyO3)dep:ocas-py
gplnoEnable GPL-licensed feature modulesdep:ocas-gpl
eggnoEnable E-graph simplification (the egg_simplify function)ocas-rewrite/egg
jitnoEnable Cranelift JIT compilation (the compile_jit method)ocas-eval/jit
simdnoEnable SIMD batch evaluation (VectorEvaluator)ocas-eval/simd
mimallocnoUse the mimalloc global allocator (accelerates many-small-allocation workloads)dep:mimalloc
system-libsnoLink against preinstalled system GMP/MPFR/MPC (required on MinGW)ocas-domain/system-libs
nttnoNTT number-theoretic transform acceleration for polynomial multiplication over 𝔽ₚocas-poly/ntt
sprsnoSparse matrix backend (used for the F4 prep phase)ocas-poly/sprs
fast-polynofast_polynomial Estrin evaluation accelerationocas-eval/fast-poly

Tip: default = [] — all backends are off by default for maximum portability (including Windows MSVC). Recommended development configuration: features = ["gmp", "jit"].

Subpage Index

TopicFileContents
Expression systemrust-expressionsArena, Atom, AtomArena, Symbol, pattern matching, parsing
Coefficient domainsrust-domainsDomain trait, integers/rationals/finite fields/real balls/complex/doubles/algebraic number fields, assumption system
Polynomialsrust-polynomialsDense univariate, sparse multivariate, rational function fields, monomial orders
Matricesrust-matrixMatrix (Bareiss determinant, Gaussian elimination)
Calculusrust-calculusdiff, integrate, taylor, integration pipeline layers
Solversrust-solversLinear systems, Diophantine equations, polynomial systems, ODE solving
Rewriting & simplificationrust-rewritePattern, Rule, simplify, transform, E-graph
Evaluation & JITrust-evaluationExpressionEvaluator, JIT, SIMD, FunctionMap
Automatic differentiationrust-autodiffDualShape, HyperDual, DualCoeff
Tensorsrust-tensorsTensor, contraction, symmetry, canonicalization, Young projection
Number theoryrust-ntheoryPrimality, factorization, discrete logarithms, CRT, number-theoretic functions, quadratic residues
Gröbner bases & idealsrust-groebnerBuchberger/F4/F5, FGLM, ideal operations, primary decomposition
Factoringrust-factoringUnivariate/multivariate ℤ[x]/𝔽ₚ[x] factoring, algebraic number fields, rational functions
Numeric integrationrust-numeric-integration.mdVegas Monte Carlo, integrate_1d

Quick Start

use ocas::prelude::*;
use ocas_core::arena::Arena;

fn main() {
    // 1. Create the arena and expression context
    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);

    // 2. Build the expression sin(x)
    let x = ctx.var("x");
    let expr = ctx.fun("sin", &[x]);

    // 3. Differentiate symbolically (diff takes a Symbol as the variable)
    let deriv = diff(&ctx, expr, Symbol::new("x"));
    assert_eq!(deriv.to_string(), "cos(x)");

    // 4. Simplify (an empty rule set keeps the expression unchanged)
    let simplified = simplify(&ctx, expr, &[], 100);
    assert_eq!(simplified.to_string(), "sin(x)");

    println!("d/dx sin(x) = {}", deriv);
}

See Also

Rust API Reference: Expression System

This chapter documents the core types and functions of the oCAS expression system: the Arena allocator, symbols, expression nodes, construction contexts, parsing, normalization, and pattern matching.


Core Type Overview

The core type hierarchy of the expression system is:

Arena (bump allocator)
 └── AtomArena (hash-consing constructor)
      └── Atom (Copy handle)
           └── AtomNode (enum: Num/Var/Fun/Add/Mul/Pow)
                └── Symbol (interned string)

Design invariants: Atom is a Copy arena handle pointing to an AtomNode allocated in the Arena. Because AtomArena uses hash-consing (structurally identical subexpressions return the same pointer within the same AtomArena), structural equality is equivalent to pointer equality — the result of == agrees with pointer comparison. Note the implementation detail: Atom's PartialEq is a derived structural comparison (recursively comparing AtomNodes); hash-consing guarantees its truth value agrees with pointer equality rather than comparing pointers directly. Subtraction and division are desugared at parse time: $x - y$ becomes Add([x, Mul([Num(-1), y])]), and $x / y$ becomes Mul([x, Pow(y, Num(-1))]).


Arena

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;
}

Arena is a bump allocator that provides bulk memory management for expression nodes. All expression nodes are allocated in an Arena, and the entire memory is freed at once when the Arena is dropped. The current version does not run destructors, so it is only safe to store Copy types.

Arena::new

#![allow(unused)]
fn main() {
pub fn new() -> Self
}

Function: Creates an Arena using the default block size (64 KiB).

Parameters: None.

Return value: Arena.

Example:

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;

let arena = Arena::new();
let value = arena.allocate_with(|| 42);
assert_eq!(*value, 42);
}

Arena::with_capacity

#![allow(unused)]
fn main() {
pub fn with_capacity(block_size: usize) -> Self
}

Function: Creates an Arena using the given block size.

Parameters:

ParameterTypeDescription
block_sizeusizeSize of each memory block, in bytes.

Return value: Arena.

Arena::allocate_with

#![allow(unused)]
fn main() {
pub fn allocate_with<T>(&self, init: impl FnOnce() -> T) -> &mut T
}

Function: Allocates a value in the Arena, constructed via the closure, and returns a mutable reference bound to the Arena's lifetime.

Parameters:

ParameterTypeDescription
initimpl FnOnce() -> TClosure that constructs the value.

Return value: &mut T.

Errors: Panics if T has zero size.

Example:

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;

let arena = Arena::new();
let value = arena.allocate_with(|| "hello");
assert_eq!(*value, "hello");
}

Arena::allocate_slice

#![allow(unused)]
fn main() {
pub fn allocate_slice<T: Copy>(&self, values: &[T]) -> &[T]
}

Function: Copies a slice into the Arena and returns a slice reference bound to the Arena's lifetime. An empty slice returns &[].

Parameters:

ParameterTypeDescription
values&[T]Slice to allocate, T: Copy.

Return value: &[T].

Example:

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;

let arena = Arena::new();
let data = [1, 2, 3];
let slice = arena.allocate_slice(&data);
assert_eq!(slice, &[1, 2, 3]);
}

Arena::reset

#![allow(unused)]
fn main() {
pub fn reset(&self)
}

Function: Resets the Arena — keeps the first memory block and resets the offset, freeing the remaining blocks. After a reset, any references allocated previously must not be used.

Parameters: None.

Return value: None.

Example:

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;

let arena = Arena::new();
let _ = arena.allocate_with(|| 1);
arena.reset();
let value = arena.allocate_with(|| 2);
assert_eq!(*value, 2);
}

Arena::chunk_count

#![allow(unused)]
fn main() {
pub fn chunk_count(&self) -> usize
}

Function: Returns the number of memory blocks currently held by the Arena.

Parameters: None.

Return value: usize.


Symbol

#![allow(unused)]
fn main() {
use ocas_atom::Symbol;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Symbol(&'static str);
}

Symbol is a globally interned symbol name used for variable names, function names, and constant names. Symbols with identical contents point to the same static memory within the process, making comparisons $O(1)$ pointer comparisons. Symbol implements Copy.

Symbol::new

#![allow(unused)]
fn main() {
pub fn new(name: &str) -> Self
}

Function: Creates or looks up an existing Symbol. The string is interned on first call; subsequent calls return the existing Symbol.

Parameters:

ParameterTypeDescription
name&strThe symbol name.

Return value: Symbol.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::Symbol;

let x = Symbol::new("x");
let also_x = Symbol::new("x");
assert_eq!(x, also_x);
assert_eq!(x.as_str(), "x");
}

Symbol::as_str

#![allow(unused)]
fn main() {
pub fn as_str(&self) -> &str
}

Function: Returns the string slice of the symbol.

Parameters: None.

Return value: &str.

See also: Atom, AtomArena


Atom

#![allow(unused)]
fn main() {
use ocas_atom::Atom;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Atom<'a>(&'a AtomNode<'a>);
}

Atom is a lightweight Copy handle to an AtomNode in the Arena. Copying an Atom only copies a pointer, not the underlying data. Thanks to hash-consing, the truth value of a == b agrees with pointer comparison (within the same AtomArena); the implementation is a derived structural comparison.

Atom implements Display, with the following output formats:

  • Num(42) → 42
  • Var("x") → x
  • Fun("sin", [x]) → sin(x)
  • Add([x, y]) → x + y (subexpressions are parenthesized unless they are leaf nodes)
  • Mul([x, y]) → x*y
  • Pow(x, n) → x^n (base/exponent parenthesized unless they are leaves)

Atom::node

#![allow(unused)]
fn main() {
pub fn node(&self) -> &'a AtomNode<'a>
}

Function: Returns a reference to the underlying node data. Used for pattern-matching the expression structure.

Parameters: None.

Return value: &'a AtomNode<'a>.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, AtomNode};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
assert!(matches!(x.node(), AtomNode::Var(_)));
}

Atom::children

#![allow(unused)]
fn main() {
pub fn children(&self) -> &'a [Atom<'a>]
}

Function: Returns the slice of direct child expressions (left to right). Num and Var return an empty slice; Fun, Add, and Mul return their argument slices; Pow returns an empty slice (use binary_children to get the two operands).

Parameters: None.

Return value: &'a [Atom<'a>].

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y, ctx.num(1)]);
assert_eq!(sum.children().len(), 3);
assert_eq!(x.children().len(), 0);
}

Atom::binary_children

#![allow(unused)]
fn main() {
pub fn binary_children(&self) -> Option<(Atom<'a>, Atom<'a>)>
}

Function: If this Atom is a Pow node, returns (base, exp); otherwise returns None.

Parameters: None.

Return value: Option<(Atom<'a>, Atom<'a>)>.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let power = ctx.pow(x, y);
let (base, exp) = power.binary_children().unwrap();
assert_eq!(base.to_string(), "x");
assert_eq!(exp.to_string(), "y");
}

See also: Atom::children, AtomNode


AtomNode

#![allow(unused)]
fn main() {
use ocas_atom::AtomNode;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum AtomNode<'a> {
    Num(i64),
    Var(Symbol),
    Fun(Symbol, &'a [Atom<'a>]),
    Add(&'a [Atom<'a>]),
    Mul(&'a [Atom<'a>]),
    Pow(Atom<'a>, Atom<'a>),
}
}

The concrete data of each node in the expression tree. An Atom handle obtains an AtomNode reference via the node() method.

Variant descriptions

VariantDescription
Num(i64)64-bit signed integer literal.
Var(Symbol)Named variable or constant.
Fun(Symbol, &'a [Atom<'a>])Function application. The first argument is the function name; the second is the argument list (at least one element).
Add(&'a [Atom<'a>])Addition. The argument list has at least one element.
Mul(&'a [Atom<'a>])Multiplication. The argument list has at least one element.
Pow(Atom<'a>, Atom<'a>)Exponentiation. The first argument is the base; the second is the exponent.

Design notes:

  • Subtraction/division are desugared at parse time: $x - y$ becomes Add([x, Mul([Num(-1), y])]), and $x / y$ becomes Mul([x, Pow(y, Num(-1))]). There are no standalone subtraction or division nodes in the AST.
  • The argument lists of Add and Mul are sorted after normalize, ensuring that structurally equal expressions produce the same AST.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, AtomNode};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
match x.node() {
    AtomNode::Var(s) => assert_eq!(s.as_str(), "x"),
    _ => panic!("expected variable"),
}
}

See also: Atom, Symbol


AtomArena

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
}
#![allow(unused)]
fn main() {
pub struct AtomArena<'a> {
    arena: &'a Arena,
    cons_table: RefCell<FastHashMap<AtomNode<'a>, Atom<'a>>>,
}
}

AtomArena is the only entry point for constructing Atoms. It wraps an Arena reference and a hash-consing table (interior mutability via RefCell). All construction methods are immutable from the caller's perspective. Structurally identical subexpressions always return the same Atom handle within the same AtomArena — this makes structural equality equivalent to pointer equality.

AtomArena::new

#![allow(unused)]
fn main() {
pub fn new(arena: &'a Arena) -> Self
}

Function: Creates an AtomArena backed by the given Arena.

Parameters:

ParameterTypeDescription
arena&'a ArenaReference to the bump allocator.

Return value: AtomArena<'a>.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let n = ctx.num(42);
assert_eq!(n.to_string(), "42");
}

AtomArena::num

#![allow(unused)]
fn main() {
pub fn num(&self, value: i64) -> Atom<'a>
}

Function: Creates an integer literal Atom.

Parameters:

ParameterTypeDescription
valuei64The integer value.

Return value: Atom<'a> — a Num(value) node.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let n = ctx.num(7);
assert_eq!(n.to_string(), "7");
}

AtomArena::var

#![allow(unused)]
fn main() {
pub fn var(&self, name: &str) -> Atom<'a>
}

Function: Creates a variable Atom.

Parameters:

ParameterTypeDescription
name&strThe variable name.

Return value: Atom<'a> — a Var(Symbol) node.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
assert_eq!(x.to_string(), "x");
}

AtomArena::fun

#![allow(unused)]
fn main() {
pub fn fun(&self, name: &str, args: &[Atom<'a>]) -> Atom<'a>
}

Function: Creates a function-application Atom.

Parameters:

ParameterTypeDescription
name&strThe function name.
args&[Atom<'a>]Argument list. Panics on an empty list in debug mode.

Return value: Atom<'a> — a Fun(Symbol, &[Atom]) node.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let f = ctx.fun("sin", &[x]);
assert_eq!(f.to_string(), "sin(x)");
}

AtomArena::add

#![allow(unused)]
fn main() {
pub fn add(&self, args: &[Atom<'a>]) -> Atom<'a>
}

Function: Creates an addition Atom.

Parameters:

ParameterTypeDescription
args&[Atom<'a>]Operand list. Panics on an empty list in debug mode.

Return value: Atom<'a> — an Add(&[Atom]) node.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y]);
assert_eq!(sum.to_string(), "x + y");
}

AtomArena::mul

#![allow(unused)]
fn main() {
pub fn mul(&self, args: &[Atom<'a>]) -> Atom<'a>
}

Function: Creates a multiplication Atom.

Parameters:

ParameterTypeDescription
args&[Atom<'a>]Operand list. Panics on an empty list in debug mode.

Return value: Atom<'a> — a Mul(&[Atom]) node.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let product = ctx.mul(&[x, y]);
assert_eq!(product.to_string(), "x*y");
}

AtomArena::pow

#![allow(unused)]
fn main() {
pub fn pow(&self, base: Atom<'a>, exp: Atom<'a>) -> Atom<'a>
}

Function: Creates an exponentiation Atom.

Parameters:

ParameterTypeDescription
baseAtom<'a>The base.
expAtom<'a>The exponent.

Return value: Atom<'a> — a Pow(base, exp) node.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let p = ctx.pow(x, ctx.num(3));
assert_eq!(p.to_string(), "x^3");
}

AtomArena::slice

#![allow(unused)]
fn main() {
pub fn slice(&self, atoms: &[Atom<'a>]) -> &'a [Atom<'a>]
}

Function: Allocates a slice of Atoms into the Arena and returns the reference. Used for multi-result scenarios that share the Arena lifetime (e.g., component solutions of ODE systems).

Parameters:

ParameterTypeDescription
atoms&[Atom<'a>]Slice of Atoms to allocate.

Return value: &'a [Atom<'a>].

See also: Arena, Atom


Functions

parse

#![allow(unused)]
fn main() {
use ocas_parse::parse;
}
#![allow(unused)]
fn main() {
pub fn parse<'a>(ctx: &'a AtomArena<'a>, input: &str) -> Result<Atom<'a>, ParseError>
}

Function: Parses a mathematical expression string into an Atom tree. Supports integers, variables, function calls (f(x)), addition, subtraction, multiplication, division, exponentiation, and parentheses. Subtraction desugars to addition with a negative coefficient; division desugars to multiplication with a negative exponent.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The arena context used to allocate nodes.
input&strThe expression string to parse.

Return value: Result<Atom<'a>, ParseError>.

Errors:

VariantDescription
ParseError::Lex(LexError)The input contains an illegal character.
ParseError::UnexpectedEofThe input ends unexpectedly (e.g., "x +").
ParseError::UnexpectedTokenAn unexpected token is encountered (e.g., "*x").

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_parse::parse;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse(&ctx, "x^2 + 2*x + 1").unwrap();
assert_eq!(expr.to_string(), "((x^2) + (2*x)) + 1");
}

See also: normalize

normalize

#![allow(unused)]
fn main() {
use ocas_atom::normalize::normalize;
}
#![allow(unused)]
fn main() {
pub fn normalize<'a>(ctx: &AtomArena<'a>, atom: Atom<'a>) -> Atom<'a>
}

Function: Normalizes an expression into a deterministic canonical form. Specifically:

  • Flattens nested Add and Mul (e.g., Add([Add([x, y]), z]) → Add([x, y, z]))
  • Sorts the operands
  • Combines numeric coefficients (e.g., Add([x, Num(2), Num(3)]) → Add([Num(5), x]))

The result is allocated in the same arena as the input.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The arena context used to allocate the result.
atomAtom<'a>The expression to normalize.

Return value: Atom<'a> — the normalized expression.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::normalize::normalize;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let z = ctx.var("z");
let inner = ctx.add(&[x, y]);
let outer = ctx.add(&[inner, z, ctx.num(2), ctx.num(3)]);
let result = normalize(&ctx, outer);
assert_eq!(result.to_string(), "5 + x + y + z");
}

See also: parse, transform

substitute

#![allow(unused)]
fn main() {
use ocas_calc::series::substitute;
}
#![allow(unused)]
fn main() {
pub fn substitute<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    replacement: Atom<'a>,
) -> Atom<'a>
}

Function: Replaces every occurrence of the variable var in the expression with replacement. A convenience function implemented on top of transform.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The arena context.
exprAtom<'a>The expression to substitute into.
varSymbolThe variable name to replace.
replacementAtom<'a>The replacement expression.

Return value: Atom<'a> — the substituted expression.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::series::substitute;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let expr = ctx.add(&[ctx.pow(x, ctx.num(2)), ctx.num(1)]);
let result = substitute(&ctx, expr, Symbol::new("x"), y);
assert_eq!(result.to_string(), "(y^2) + 1");
}

See also: transform

transform

#![allow(unused)]
fn main() {
use ocas_rewrite::transform;
}
#![allow(unused)]
fn main() {
pub fn transform<'a, F>(ctx: &'a AtomArena<'a>, atom: Atom<'a>, mut f: F) -> Atom<'a>
where
    F: FnMut(Atom<'a>) -> Option<Atom<'a>>,
}

Function: Traverses the expression tree bottom-up and applies the transform function f to each node. f is called after the node's children have been transformed. Returning Some(atom) replaces the node; returning None keeps the node (with its transformed children). This is the standard rewrite traversal pattern used by the oCAS rule engine and simplifier.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The arena context.
atomAtom<'a>The expression to transform.
fFnMut(Atom<'a>) -> Option<Atom<'a>>The transform function.

Return value: Atom<'a> — the transformed expression.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{Atom, AtomArena, AtomNode};
use ocas_core::arena::Arena;
use ocas_rewrite::transform;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y]);

let result = transform(&ctx, sum, |a| {
    if let AtomNode::Add(args) = a.node() {
        if args.len() == 2 && args[0] == x && args[1] == y {
            return Some(ctx.add(&[y, x]));
        }
    }
    None
});

assert_eq!(result.to_string(), "y + x");
}

See also: substitute, normalize

collect_funs

#![allow(unused)]
fn main() {
use ocas_atom::walk::collect_funs;
}
#![allow(unused)]
fn main() {
pub fn collect_funs<'a>(atom: Atom<'a>) -> Vec<(Symbol, Atom<'a>)>
}

Function: Collects all function applications in the expression in post-order (innermost first), deduplicated (hash-consing guarantees that structurally identical applications appear only once).

Parameters:

ParameterTypeDescription
atomAtom<'a>The expression to traverse.

Return value: Vec<(Symbol, Atom<'a>)> — a list of (function name, function application node).

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, walk::collect_funs};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let sin_x = ctx.fun("sin", &[x]);
let expr = ctx.fun("cos", &[sin_x]);
let funs = collect_funs(expr);
assert_eq!(funs.len(), 2);
assert_eq!(funs[0].0.as_str(), "sin");
assert_eq!(funs[1].0.as_str(), "cos");
}

See also: collect_vars

collect_vars

#![allow(unused)]
fn main() {
use ocas_atom::walk::collect_vars;
}
#![allow(unused)]
fn main() {
pub fn collect_vars(atom: Atom) -> Vec<Symbol>
}

Function: Collects all distinct variable names in the expression, in order of first appearance (depth-first, left to right).

Parameters:

ParameterTypeDescription
atomAtomThe expression to traverse.

Return value: Vec<Symbol> — the list of variable names.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, walk::collect_vars};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let expr = ctx.add(&[ctx.mul(&[x, y]), x]);
let vars = collect_vars(expr);
assert_eq!(vars.len(), 2);
assert_eq!(vars[0].as_str(), "x");
assert_eq!(vars[1].as_str(), "y");
}

See also: collect_funs


Pattern Matching

The pattern-matching system matches Atom expression trees against patterns with wildcards (Pattern), with support for associative/commutative (AC) matching and backtracking-budget control.

Pattern

#![allow(unused)]
fn main() {
use ocas_rewrite::pattern::Pattern;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Pattern<'a> {
    Literal(Atom<'a>),
    Wildcard(Symbol, WildcardLevel),
    Add(Vec<Pattern<'a>>),
    Mul(Vec<Pattern<'a>>),
    Pow(Box<(Pattern<'a>, Pattern<'a>)>),
    Fun(Symbol, Vec<Pattern<'a>>),
}
}

Patterns mirror the structure of AtomNode but add wildcard nodes. Matching of Add and Mul supports associative-commutative (AC) matching: subpatterns can match any subset of the argument list.

VariantDescription
Literal(Atom)Matches the given Atom exactly.
Wildcard(Symbol, WildcardLevel)Wildcard match; the name and level determine the binding behavior.
Add(Vec<Pattern>)Matches an Add node; the argument list is matched AC-wise.
Mul(Vec<Pattern>)Matches a Mul node; the argument list is matched AC-wise.
Pow(Box<(Pattern, Pattern)>)Matches a Pow node; base and exponent are matched separately.
Fun(Symbol, Vec<Pattern>)Matches a Fun node; function name and argument list are matched separately.

Pattern::from_atom

#![allow(unused)]
fn main() {
pub fn from_atom(_ctx: &'a impl PatternAlloc<'a>, atom: Atom<'a>) -> Pattern<'a>
}

Function: Converts an Atom into a Pattern. Variables whose names end in _ (or begin with _) are treated as wildcards:

  • x_ → Wildcard(Symbol("x"), Single)
  • x__ → Wildcard(Symbol("x"), Sequence)
  • x___ → Wildcard(Symbol("x"), NullSequence)

Parameters:

ParameterTypeDescription
_ctx&'a impl PatternAlloc<'a>The pattern allocator (usually &()).
atomAtom<'a>The expression to convert.

Return value: Pattern<'a>.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_rewrite::pattern::{Pattern, WildcardLevel};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x_");
let pat = Pattern::from_atom(&(), x);
assert!(matches!(pat, Pattern::Wildcard(s, WildcardLevel::Single) if s.as_str() == "x"));
}

See also: WildcardLevel, match_pattern

WildcardLevel

#![allow(unused)]
fn main() {
use ocas_rewrite::pattern::WildcardLevel;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WildcardLevel {
    Single,
    Sequence,
    NullSequence,
}
}

The matching scope of a wildcard.

VariantNaming conventionDescription
SingleOne trailing _ in the name (e.g., x_; a leading _ also works, e.g. _x)Matches exactly one Atom.
SequenceTwo trailing _ in the name (e.g., x__; two leading _ also work, e.g. __x)Matches one or more Atoms in an Add/Mul/Fun argument list.
NullSequenceThree trailing _ in the name (e.g., x___; three leading _ also work, e.g. ___x)Matches zero or more Atoms.

Example:

#![allow(unused)]
fn main() {
use ocas_rewrite::pattern::WildcardLevel;

assert!(matches!(WildcardLevel::Single, WildcardLevel::Single));
assert!(matches!(WildcardLevel::Sequence, WildcardLevel::Sequence));
assert!(matches!(WildcardLevel::NullSequence, WildcardLevel::NullSequence));
}

match_pattern

#![allow(unused)]
fn main() {
use ocas_rewrite::matcher::match_pattern;
}
#![allow(unused)]
fn main() {
pub fn match_pattern<'a>(pattern: Pattern<'a>, atom: Atom<'a>) -> Result<Bindings<'a>, MatchError>
}

Function: Attempts to match pattern against atom, returning the binding set on success. Uses the default backtracking budget (DEFAULT_MAX_BACKTRACKS = 10_000). Add/Mul use AC matching (full backtracking search); Fun uses ordered matching (with sequence wildcard support).

Parameters:

ParameterTypeDescription
patternPattern<'a>The matching pattern.
atomAtom<'a>The expression to match against.

Return value: Result<Bindings<'a>, MatchError>.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;
use ocas_rewrite::pattern::{Pattern, WildcardLevel};
use ocas_rewrite::matcher::match_pattern;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let sum = ctx.add(&[x, ctx.num(1)]);

let pat = Pattern::Add(vec![
    Pattern::Wildcard(Symbol::new("a"), WildcardLevel::Single),
    Pattern::Literal(ctx.num(1)),
]);
let bindings = match_pattern(pat, sum).unwrap();
}

match_pattern_with_budget

#![allow(unused)]
fn main() {
use ocas_rewrite::matcher::match_pattern_with_budget;
}
#![allow(unused)]
fn main() {
pub fn match_pattern_with_budget<'a>(
    pattern: Pattern<'a>,
    atom: Atom<'a>,
    max_backtracks: usize,
) -> Result<Bindings<'a>, MatchError>
}

Function: Same as match_pattern, but allows a custom backtracking budget to prevent exponential-time blowups on pathological inputs.

Parameters:

ParameterTypeDescription
patternPattern<'a>The matching pattern.
atomAtom<'a>The expression to match against.
max_backtracksusizeThe maximum number of backtracks.

Return value: Result<Bindings<'a>, MatchError>.

Errors: Returns MatchError::BudgetExhausted if the backtracking limit is exceeded.

See also: match_pattern

Bindings

#![allow(unused)]
fn main() {
use ocas_rewrite::matcher::Bindings;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Default)]
pub struct Bindings<'a> { /* private fields */ }
}

The set of wildcard bindings produced by a successful match.

Bindings::new

#![allow(unused)]
fn main() {
pub fn new() -> Self
}

Function: Creates an empty binding set.

Bindings::get

#![allow(unused)]
fn main() {
pub fn get(&self, name: Symbol) -> Option<&MatchValue<'a>>
}

Function: Looks up the bound value by wildcard name.

Parameters:

ParameterTypeDescription
nameSymbolThe wildcard name (without trailing underscores).

Return value: Option<&MatchValue<'a>>.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;
use ocas_rewrite::pattern::{Pattern, WildcardLevel};
use ocas_rewrite::matcher::match_pattern;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y]);

let pat = Pattern::Add(vec![
    Pattern::Wildcard(Symbol::new("a"), WildcardLevel::Single),
    Pattern::Wildcard(Symbol::new("b"), WildcardLevel::Single),
]);
let bindings = match_pattern(pat, sum).unwrap();
let val = bindings.get(Symbol::new("a")).unwrap();
}

MatchValue

#![allow(unused)]
fn main() {
use ocas_rewrite::matcher::MatchValue;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchValue<'a> {
    Single(Atom<'a>),
    Sequence(&'a [Atom<'a>]),
}
}

The value type of a wildcard binding.

VariantDescription
Single(Atom<'a>)A Single-level wildcard bound to a single Atom.
Sequence(&'a [Atom<'a>])A Sequence- or NullSequence-level wildcard bound to a slice of Atoms.

MatchError

#![allow(unused)]
fn main() {
use ocas_rewrite::matcher::MatchError;
}
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchError {
    NoMatch,
    InconsistentBinding,
    BudgetExhausted,
}
}

Pattern-matching errors.

VariantDisplayDescription
NoMatch"pattern did not match"The pattern does not match the target expression.
InconsistentBinding"inconsistent wildcard binding"The same wildcard name is bound to different values at different positions.
BudgetExhausted"backtrack budget exhausted"The backtracking budget of AC matching is exhausted. Indicates the input may cause exponential-time matching; increase the budget or simplify the pattern.

See also

Rust API Reference: Coefficient Domains

Coefficient domains are the foundational abstraction behind all of oCAS's polynomial, matrix, and solver algorithms. Each domain describes a set of elements and their arithmetic operations, unified through the Domain and EuclideanDomain traits.

Module path: ocas_domain

Imports:

#![allow(unused)]
fn main() {
use ocas_domain::{
    Domain, EuclideanDomain,
    Integer, IntegerDomain,
    Rational, RationalDomain,
    FiniteField, FiniteFieldElement,
    RealBall, RealBallDomain,
    Complex, ComplexDomain,
    DoubleF64, DoubleF64Domain,
    AlgebraicExtension, AlgebraicNumberField, AlgebraicElement,
};
use ocas_domain::assumptions::{Assumption, Assumptions, SymbolAssumptions};
}

Domain trait

Signature:

#![allow(unused)]
fn main() {
pub trait Domain: Clone + PartialEq + Eq + std::fmt::Debug + Sized {
    type Element: Clone + PartialEq + Eq + std::fmt::Debug + 'static;

    fn zero(&self) -> Self::Element;
    fn one(&self) -> Self::Element;
    fn add(&self, a: &Self::Element, b: &Self::Element) -> Self::Element;
    fn sub(&self, a: &Self::Element, b: &Self::Element) -> Self::Element;
    fn neg(&self, a: &Self::Element) -> Self::Element;
    fn mul(&self, a: &Self::Element, b: &Self::Element) -> Self::Element;
    fn div(&self, a: &Self::Element, b: &Self::Element) -> Option<Self::Element>;
    fn inv(&self, a: &Self::Element) -> Option<Self::Element>;
    fn is_zero(&self, a: &Self::Element) -> bool;
    fn is_one(&self, a: &Self::Element) -> bool;
    fn mul_assign(&self, a: &mut Self::Element, b: &Self::Element);
    fn sub_mul_assign(&self, a: &mut Self::Element, b: &Self::Element, c: &Self::Element);
    fn pow(&self, a: &Self::Element, n: u64) -> Self::Element;
    fn cast_u64(&self, n: u64) -> Self::Element;
}
}

Description: The core trait for coefficient domains. The domain object itself may carry parameters (such as the modulus of a finite field), so all operations go through &self. This follows the "domain object" pattern used by CAS libraries such as Flint and SymPy's Domain.

Associated types:

Associated typeBoundsDescription
ElementClone + PartialEq + Eq + Debug + 'staticThe type of elements in the domain

Methods:

MethodSignatureDescription
zerofn zero(&self) -> Self::ElementAdditive identity
onefn one(&self) -> Self::ElementMultiplicative identity
addfn add(&self, a, b) -> Self::ElementAddition
subfn sub(&self, a, b) -> Self::ElementSubtraction
negfn neg(&self, a) -> Self::ElementNegation
mulfn mul(&self, a, b) -> Self::ElementMultiplication
divfn div(&self, a, b) -> Option<Self::Element>Division. Returns None if b is zero or the division is not exact
invfn inv(&self, a) -> Option<Self::Element>Multiplicative inverse. Returns None if a is zero
is_zerofn is_zero(&self, a) -> boolWhether a is the additive identity (default: *a == self.zero())
is_onefn is_one(&self, a) -> boolWhether a is the multiplicative identity (default: *a == self.one())
mul_assignfn mul_assign(&self, a: &mut E, b: &E)In-place multiplication *a *= b. Defaults to creating a new element; high-performance domains such as GMP may override
sub_mul_assignfn sub_mul_assign(&self, a: &mut E, b: &E, c: &E)Fused subtract-multiply *a -= b * c. Used heavily in F4 row echelonization
powfn pow(&self, a, n: u64) -> Self::ElementNon-negative integer power. Defaults to binary exponentiation; finite fields override with modpow
cast_u64fn cast_u64(&self, n: u64) -> Self::ElementConverts a u64 to a domain element. Defaults to adding one repeatedly

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Domain, Integer, IntegerDomain};

let domain = IntegerDomain;
let a = Integer::from(3);
let b = Integer::from(5);
assert_eq!(domain.add(&a, &b), Integer::from(8));
assert_eq!(domain.mul(&a, &b), Integer::from(15));
assert_eq!(domain.pow(&a, 3), Integer::from(27));
// Output: all assertions pass
}

See also: EuclideanDomain


EuclideanDomain trait

Signature:

#![allow(unused)]
fn main() {
pub trait EuclideanDomain: Domain {
    fn div_rem(&self, a: &Self::Element, b: &Self::Element)
        -> Option<(Self::Element, Self::Element)>;
    fn gcd(&self, a: &Self::Element, b: &Self::Element) -> Self::Element;
    fn extended_gcd(&self, a: &Self::Element, b: &Self::Element)
        -> (Self::Element, Self::Element, Self::Element);
}
}

Description: A Euclidean domain supporting division with remainder. An extension of Domain that additionally provides div_rem (division with remainder), gcd (greatest common divisor), and extended_gcd (extended Euclidean algorithm).

Methods:

div_rem

Signature: fn div_rem(&self, a: &Self::Element, b: &Self::Element) -> Option<(Self::Element, Self::Element)>

Description: Division with remainder, returning (quotient, remainder) such that a = quotient * b + remainder and remainder == 0 or deg(remainder) < deg(b).

Parameters:

ParameterTypeDescription
a&Self::ElementDividend
b&Self::ElementDivisor

Return value: Some((quotient, remainder)), or None when b is zero.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{EuclideanDomain, Integer, IntegerDomain};

let domain = IntegerDomain;
let a = Integer::from(17);
let b = Integer::from(5);
let (q, r) = domain.div_rem(&a, &b).unwrap();
assert_eq!(q, Integer::from(3));
assert_eq!(r, Integer::from(2));
// Output: 17 = 3 × 5 + 2
}

gcd

Signature: fn gcd(&self, a: &Self::Element, b: &Self::Element) -> Self::Element

Description: Computes the greatest common divisor. The default implementation uses the Euclidean algorithm.

Return value: gcd(a, b). For a field (such as FiniteField), the GCD of two non-zero elements degenerates to 1.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{EuclideanDomain, Integer, IntegerDomain};

let domain = IntegerDomain;
let a = Integer::from(12);
let b = Integer::from(8);
let g = domain.gcd(&a, &b);
assert_eq!(g, Integer::from(4));
// Output: gcd(12, 8) = 4
}

extended_gcd

Signature: fn extended_gcd(&self, a: &Self::Element, b: &Self::Element) -> (Self::Element, Self::Element, Self::Element)

Description: Extended Euclidean algorithm, returning (g, x, y) such that g = gcd(a, b) = a * x + b * y.

Return value: The triple (g, x, y).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{EuclideanDomain, Integer, IntegerDomain};

let domain = IntegerDomain;
let a = Integer::from(35);
let b = Integer::from(15);
let (g, x, y) = domain.extended_gcd(&a, &b);
// g = 5, x = 1, y = -2, so that 5 = 35×1 + 15×(-2)
assert_eq!(g, Integer::from(5));
}

See also: Domain


Integer / IntegerDomain

IntegerDomain

Signature: pub struct IntegerDomain;

Description: The integer domain $\mathbb{Z}$, with element type Integer. Implements Domain and EuclideanDomain.

Characteristics:

  • div requires exact division (zero remainder), otherwise returns None
  • inv returns a result only for $\pm 1$, otherwise None
  • pow uses binary exponentiation

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Domain, EuclideanDomain, Integer, IntegerDomain};

let domain = IntegerDomain;
let a = Integer::from(10);
let b = Integer::from(3);

// Exact division: 10/3 is not exact
assert!(domain.div(&a, &b).is_none());

// Division with remainder
let (q, r) = domain.div_rem(&a, &b).unwrap();
assert_eq!(q, Integer::from(3));
assert_eq!(r, Integer::from(1));

// Inverse: only ±1 have inverses
assert!(domain.inv(&Integer::from(1)).is_some());
assert!(domain.inv(&Integer::from(5)).is_none());
// Output: all assertions pass
}

Integer

Signature: pub struct Integer(BigInt);

Description: An arbitrary-precision integer. The default build uses num-bigint's BigInt; with the gmp feature enabled, a GMP backend is used.

Derives: Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash

Construction and conversion

Integer::new

Signature: pub fn new<T: Into<BigInt>>(value: T) -> Self

Description: Creates an arbitrary-precision integer from a machine integer or a BigInt.

Parameters:

ParameterTypeDescription
valueT: Into<BigInt>The integer value (i32, i64, u64, BigInt, etc.)

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;

let a = Integer::new(42);
let b = Integer::new(100_i64);
assert_eq!(a.to_string(), "42");
// Output: 42
}
Integer::from

Signature: impl From<i64> for Integer / impl From<BigInt> for Integer

Description: Conversion from i64 or BigInt.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;

let a = Integer::from(42);
assert_eq!(a.to_string(), "42");
// Output: 42
}

Accessors

Integer::inner

Signature: pub fn inner(&self) -> &BigInt

Description: Accesses the underlying BigInt reference.

Integer::to_bigint

Signature: pub fn to_bigint(&self) -> BigInt

Description: Clones into a BigInt (regardless of backend).

Integer::to_i64

Signature: pub fn to_i64(&self) -> Option<i64>

Description: Attempts conversion to i64. Returns None on overflow.

Arithmetic methods

Integer::pow_u32

Signature: pub fn pow_u32(&self, exp: u32) -> Self

Description: Computes $n^{exp}$.

Parameters:

ParameterTypeDescription
expu32Non-negative exponent

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;

let a = Integer::from(2);
assert_eq!(a.pow_u32(10).to_string(), "1024");
// Output: 1024
}
Integer::modpow

Signature: pub fn modpow(&self, exp: &Integer, modulus: &Integer) -> Integer

Description: Modular exponentiation $self^{exp} \bmod modulus$.

Parameters:

ParameterTypeDescription
exp&IntegerExponent (non-negative)
modulus&IntegerModulus (positive)

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;

let base = Integer::from(3);
let exp = Integer::from(100);
let modulus = Integer::from(7);
let result = base.modpow(&exp, &modulus);
assert_eq!(result.to_string(), "4");
// Output: 3^100 mod 7 = 4
}
Integer::mod_floor

Signature: pub fn mod_floor(&self, modulus: &Integer) -> Integer

Description: Floor modulus; the result $r$ satisfies $0 \leq r < |modulus|$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;

let a = Integer::from(-7);
let m = Integer::from(3);
assert_eq!(a.mod_floor(&m).to_string(), "2");
// Output: -7 mod_floor 3 = 2
}
Integer::div_rem

Signature: pub fn div_rem(&self, other: &Integer) -> (Integer, Integer)

Description: Division with remainder, (quotient, remainder). Note: unlike EuclideanDomain::div_rem, this method does not require other to be non-zero (behavior is determined by the underlying BigInt).

Integer::is_even

Signature: pub fn is_even(&self) -> bool

Description: Whether the integer is even.

Integer::is_negative

Signature: pub fn is_negative(&self) -> bool

Description: Whether the integer is negative.

Integer::is_zero

Signature: pub fn is_zero(&self) -> bool

Description: Whether the integer is zero.

Integer::is_one

Signature: pub fn is_one(&self) -> bool

Description: Whether the integer is one.

Integer::abs

Signature: pub fn abs(&self) -> Integer

Description: Absolute value.

Integer::sqrt

Signature: pub fn sqrt(&self) -> Integer

Description: Integer square root (floored).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;

let a = Integer::from(10);
assert_eq!(a.sqrt().to_string(), "3");
// Output: sqrt(10) = 3
}

Operators

Integer implements the following standard operator traits (all reference combinations are supported: owned × owned, owned × &, & × owned, & × &):

TraitOperation
Add+
Sub-
Mul*
Div/ (integer division, truncated toward zero)
Rem%
Negunary -
Shr<u32> / ShrAssign<u32>right shift
AddAssign<&Integer> / SubAssign<&Integer> / MulAssign<&Integer> / DivAssign<&Integer>compound assignment

See also: Domain, Rational, Number-theoretic functions


Rational / RationalDomain

RationalDomain

Signature: pub struct RationalDomain;

Description: The rational number field $\mathbb{Q}$, with element type Rational. Implements Domain and EuclideanDomain.

Characteristics:

  • div returns an exact result for every non-zero divisor
  • inv returns a result for every non-zero element
  • EuclideanDomain's div_rem degenerates over the rationals: the remainder is always zero

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Domain, Rational, RationalDomain};

let domain = RationalDomain;
let a = Rational::new(1, 2);
let b = Rational::new(1, 3);
let sum = domain.add(&a, &b);
assert_eq!(sum, Rational::new(5, 6));
// Output: 1/2 + 1/3 = 5/6
}

Rational

Signature: pub struct Rational(BigRational);

Description: An arbitrary-precision rational number. The default build uses num-rational's BigRational; with the gmp feature enabled, a GMP backend is used.

Derives: Debug, Clone, PartialEq, Eq, Hash

Construction

Rational::new

Signature: pub fn new(numer: i64, denom: i64) -> Self

Description: Creates a rational from a numerator and denominator (i64). Automatically reduced to lowest terms. Behavior for a zero denominator is determined by the underlying library.

Parameters:

ParameterTypeDescription
numeri64Numerator
denomi64Denominator (non-zero)

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Rational;

let a = Rational::new(3, 6);
assert_eq!(a.to_string(), "1/2");
// Output: 3/6 automatically reduces to 1/2
}
Rational::from_bigints

Signature: pub fn from_bigints(numer: BigInt, denom: BigInt) -> Self

Description: Creates a rational from an arbitrary-precision integer numerator and denominator.

Rational::from_integer

Signature: pub fn from_integer(n: Integer) -> Self

Description: Creates a rational from an integer (denominator = 1).

Accessors

Rational::inner

Signature: pub fn inner(&self) -> &BigRational

Description: Accesses the underlying BigRational reference.

Rational::numer

Signature: pub fn numer(&self) -> Integer

Description: Returns the numerator (as an Integer).

Rational::denom

Signature: pub fn denom(&self) -> Integer

Description: Returns the denominator (as an Integer, always positive).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Rational;

let r = Rational::new(3, 4);
assert_eq!(r.numer().to_string(), "3");
assert_eq!(r.denom().to_string(), "4");
// Output: numerator = 3, denominator = 4
}

Operators

Rational implements the following standard operator traits:

TraitOperation
Add+
Sub-
Mul*
Div/
Negunary -
AddAssign / SubAssign / MulAssign / DivAssigncompound assignment

See also: Domain, Integer


FiniteField / FiniteFieldElement

FiniteField

Signature: pub struct FiniteField { /* prime: BigInt, ... */ }

Description: The prime finite field $\mathbb{Z}/p\mathbb{Z}$. Arithmetic uses arbitrary-precision integers, supporting large primes. Implements Domain and EuclideanDomain.

Construction

FiniteField::new

Signature: pub fn new(prime: BigInt) -> Self

Description: Creates a finite field with the given prime modulus.

Parameters:

ParameterTypeDescription
primeBigIntThe modulus (must be $\geq 2$; primality is not verified)

Errors: panics in debug mode if prime < 2.

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::{Domain, FiniteField};

let f = FiniteField::new(BigInt::from(7));
let a = f.element(3);
let b = f.element(5);
assert_eq!(f.add(&a, &b), f.element(1));   // 3 + 5 = 1 (mod 7)
assert_eq!(f.mul(&a, &b), f.element(1));   // 3 × 5 = 1 (mod 7)
// Output: all assertions pass
}

Element construction

FiniteField::element

Signature: pub fn element(&self, value: impl Into<BigInt>) -> FiniteFieldElement

Description: Creates a field element from an arbitrary integer. The value is automatically reduced to $[0, p-1]$.

Parameters:

ParameterTypeDescription
valueimpl Into<BigInt>The integer value (may lie outside $[0, p-1]$; reduced automatically)

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::{Domain, FiniteField};

let f = FiniteField::new(BigInt::from(7));
let a = f.element(10);
assert_eq!(a.value().to_string(), "3");
// Output: 10 mod 7 = 3
}
FiniteField::from_i64

Signature: pub fn from_i64(&self, val: i64) -> FiniteFieldElement

Description: Creates a field element from an i64 (reduced mod p).

Accessors

FiniteField::prime

Signature: pub fn prime(&self) -> &BigInt

Description: Returns the field's modulus.

FiniteField::prime_u64

Signature: pub fn prime_u64(&self) -> u64

Description: Returns the modulus as a u64.

Errors: panics if the prime does not fit in a u64.

FiniteField::to_i64

Signature: pub fn to_i64(&self, a: &FiniteFieldElement) -> i64

Description: Converts a field element to an i64 (in the range $[0, p)$).

Errors: panics if the prime does not fit in a u64.

Domain implementation

  • div inverts via Fermat's little theorem: $a^{-1} \equiv a^{p-2} \pmod{p}$
  • inv as above; returns None for the zero element
  • pow uses the modpow optimization (much faster than the default binary exponentiation)

EuclideanDomain implementation

  • div_rem: division is exact in a field; the remainder is always zero
  • gcd: GCD degenerates in a field — returns 0 when both are zero, otherwise 1

See also: Domain, Finite field mathematics


FiniteFieldElement

Signature: pub struct FiniteFieldElement { value: BigInt }

Description: An element of a prime finite field. The value is always in $[0, p-1]$.

Derives: Debug, Clone, PartialEq, Eq, Hash

FiniteFieldElement::value

Signature: pub fn value(&self) -> &BigInt

Description: Returns the canonical representative in $[0, p-1]$.

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::FiniteField;

let f = FiniteField::new(BigInt::from(7));
let a = f.element(-3);
assert_eq!(a.value().to_string(), "4");
// Output: -3 mod 7 = 4
}

See also: FiniteField


RealBall / RealBallDomain

RealBallDomain

Signature: pub struct RealBallDomain;

Description: The real ball (interval) domain. Element type RealBall. Implements only Domain (not EuclideanDomain, because real balls do not support exact division with remainder).

Note: The default build uses lightweight f64 balls, suitable for templates and demos. With the mpfr feature enabled, rug::Float with directed rounding produces rigorous intervals.

RealBall

Signature: pub struct RealBall { mid: f64, rad: f64 } (default build)

Description: A real ball: midpoint ± radius. The true value is guaranteed to lie in $[mid - rad, mid + rad]$.

Derives: Debug, Clone, PartialEq, Eq; the default build additionally implements Copy.

Construction

RealBall::new

Signature (default): pub fn new(mid: f64, rad: f64) -> Self

Description: Creates a ball from a midpoint and radius. The radius is clamped to be non-negative.

Parameters:

ParameterTypeDescription
midf64 (or rug::Float)Midpoint
radf64 (or rug::Float)Radius ($\geq 0$)
RealBall::from_f64

Signature: pub fn from_f64(value: f64) -> Self

Description: Creates a zero-radius (exact) ball from an f64 value.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::RealBall;

let ball = RealBall::from_f64(3.14);
assert_eq!(ball.mid(), 3.14);
assert_eq!(ball.rad(), 0.0);
// Output: exact ball
}

Accessors

RealBall::mid

Signature: pub fn mid(&self) -> f64 (default) / pub fn mid(&self) -> &rug::Float (mpfr)

Description: Returns the midpoint.

RealBall::rad

Signature: pub fn rad(&self) -> f64 (default) / pub fn rad(&self) -> &rug::Float (mpfr)

Description: Returns the radius.

RealBall::lower

Signature: pub fn lower(&self) -> f64 (default) / pub fn lower(&self) -> rug::Float (mpfr)

Description: Returns the conservative lower bound $mid - rad$ (the mpfr version rounds downward).

RealBall::upper

Signature: pub fn upper(&self) -> f64 (default) / pub fn upper(&self) -> rug::Float (mpfr)

Description: Returns the conservative upper bound $mid + rad$ (the mpfr version rounds upward).

RealBall::precision

Signature: pub fn precision(&self) -> u32 (mpfr feature only)

Description: Returns the precision (in bits) of the MPFR backend.

Domain implementation

  • add: $(a \pm r_a) + (b \pm r_b) = (a+b) \pm (r_a + r_b)$ (the mpfr version additionally adds rounding error)
  • sub: like addition; the radii add
  • mul: four-corner method — compute the four extreme products $a_lo \cdot b_lo$, $a_lo \cdot b_hi$, etc., and take the min/max for the new interval
  • div: $a / b = a \cdot b^{-1}$, returns None when the ball contains zero
  • inv: $1/(mid \pm rad)$, returns None when the ball contains zero

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Domain, RealBall, RealBallDomain};

let domain = RealBallDomain;
let a = RealBall::from_f64(2.0);
let b = RealBall::from_f64(3.0);
let prod = domain.mul(&a, &b);
assert!(prod.lower() <= 6.0 && 6.0 <= prod.upper());
// Output: the product ball contains the true value 6.0
}

See also: Domain


Complex / ComplexDomain

ComplexDomain

Signature: pub struct ComplexDomain<D: Domain> { base: D, ... }

Description: The complex field built over an arbitrary base domain $D$. Element type Complex<D>. Implements only Domain.

Construction

ComplexDomain::new

Signature: pub fn new(base: D) -> Self

Description: Creates the complex field over the base domain base.

Parameters:

ParameterTypeDescription
baseD: DomainThe base domain (e.g. IntegerDomain, RationalDomain)
ComplexDomain::base

Signature: pub fn base(&self) -> &D

Description: Returns a reference to the base domain.

ComplexDomain::real_element

Signature: pub fn real_element(&self, re: D::Element) -> Complex<D>

Description: Creates a purely real element.

ComplexDomain::imag_element

Signature: pub fn imag_element(&self, im: D::Element) -> Complex<D>

Description: Creates a purely imaginary element.

Domain implementation

  • mul: $(a+bi)(c+di) = (ac-bd) + (ad+bc)i$
  • div: $(a+bi)/(c+di) = \frac{(ac+bd)+(bc-ad)i}{c^2+d^2}$, returns None when the denominator is zero
  • inv: implemented via div(one, a)

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Complex, ComplexDomain, Domain, Integer, IntegerDomain};

let domain = ComplexDomain::new(IntegerDomain);
let a = Complex::new(Integer::from(1), Integer::from(2));
let b = Complex::new(Integer::from(3), Integer::from(4));
let sum = domain.add(&a, &b);
assert_eq!(*sum.re(), Integer::from(4));
assert_eq!(*sum.im(), Integer::from(6));
// Output: (1+2i) + (3+4i) = 4+6i
}

See also: Domain


Complex

Signature: pub struct Complex<D: Domain> { inner: NumComplex<D::Element> }

Description: A complex number whose real and imaginary parts belong to the base domain $D$.

Derives: Debug, Clone, PartialEq, Eq, Hash

Construction and access

Complex::new

Signature: pub fn new(real: D::Element, imag: D::Element) -> Self

Description: Creates a complex number from its real and imaginary parts.

Complex::re

Signature: pub fn re(&self) -> &D::Element

Description: Returns a reference to the real part.

Complex::im

Signature: pub fn im(&self) -> &D::Element

Description: Returns a reference to the imaginary part.

Complex::inner

Signature: pub fn inner(&self) -> &NumComplex<D::Element>

Description: Returns a reference to the underlying num_complex::Complex.

See also: ComplexDomain


DoubleF64 / DoubleF64Domain

DoubleF64Domain

Signature: pub struct DoubleF64Domain;

Description: The double-double floating-point domain. Element type DoubleF64. Implements only Domain.

DoubleF64

Signature:

#![allow(unused)]
fn main() {
pub struct DoubleF64 {
    pub hi: f64,  // high component (principal value)
    pub lo: f64,  // low component (error term)
}
}

Description: A double-double floating-point number represented as $hi + lo$ with $|lo| \leq 0.5 \cdot \text{ulp}(hi)$. Provides about 31 decimal digits of precision (~84 bits), roughly twice the precision of a single f64.

Derives: Debug, Clone, Copy, PartialEq; implements Eq.

Arithmetic is based on the error-free transformation algorithms of Dekker and Knuth (TwoSum, TwoProd), significantly faster than arbitrary-precision alternatives such as MPFR.

Constants

ConstantValueDescription
DoubleF64::ZERO{ hi: 0.0, lo: 0.0 }Zero
DoubleF64::ONE{ hi: 1.0, lo: 0.0 }One

Construction and conversion

DoubleF64::new

Signature: pub fn new(hi: f64, lo: f64) -> Self

Description: Creates from high and low components. The caller must ensure $|lo| \leq 0.5 \cdot \text{ulp}(hi)$ for correctness.

DoubleF64::from_f64

Signature: pub fn from_f64(x: f64) -> Self

Description: Creates from a single f64 ($lo = 0$).

DoubleF64::to_f64

Signature: pub fn to_f64(self) -> f64

Description: Extracts the high component.

From<f64>

Signature: impl From<f64> for DoubleF64

Description: Equivalent to from_f64.

Error-free transformations

DoubleF64::quick_two_sum

Signature: pub fn quick_two_sum(a: f64, b: f64) -> Self

Description: Error-free summation for $|a| \geq |b|$. Faster than two_sum but requires the precondition.

DoubleF64::two_sum

Signature: pub fn two_sum(a: f64, b: f64) -> Self

Description: Dekker TwoSum — error-free summation in which the rounding error is captured exactly in the lo component.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::DoubleF64;

let s = DoubleF64::two_sum(1.0, f64::EPSILON);
assert_eq!(s.hi, 1.0 + f64::EPSILON);
// s.lo captures the rounding error
}

Queries

MethodSignatureDescription
abspub fn abs(self) -> SelfAbsolute value
is_nanpub fn is_nan(self) -> boolWhether the value is NaN
is_infinitepub fn is_infinite(self) -> boolWhether the value is infinite
is_finitepub fn is_finite(self) -> boolWhether the value is finite

Arithmetic operations

DoubleF64::add / sub / mul / div

Signature:

#![allow(unused)]
fn main() {
pub fn add(self, other: Self) -> Self
pub fn sub(self, other: Self) -> Self
pub fn mul(self, other: Self) -> Self
pub fn div(self, other: Self) -> Self
}

Description: Double-double addition, subtraction, multiplication, and division. Internally uses TwoSum/TwoProd to capture rounding errors.

DoubleF64::powi

Signature: pub fn powi(self, n: i64) -> Self

Description: Integer power using binary exponentiation. Supports negative exponents.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::DoubleF64;

let x = DoubleF64::from_f64(3.0);
assert_eq!(x.powi(3).hi, 27.0);
assert_eq!(x.powi(-1).hi, 1.0 / 3.0);
// Output: 3^3 = 27, 3^(-1) = 0.333...
}

Transcendental functions

MethodSignatureDescription
sqrtpub fn sqrt(self) -> SelfSquare root by Newton iteration. Returns NaN for negative input
exppub fn exp(self) -> SelfExponential function, Taylor series + argument reduction (by $\ln 2$)
lnpub fn ln(self) -> SelfNatural logarithm, Newton iteration. Returns NaN for non-positive input
sinpub fn sin(self) -> SelfSine, Taylor series + reduction to $[-\pi, \pi]$
cospub fn cos(self) -> SelfCosine, $\cos(x) = \sin(\pi/2 - x)$
tanpub fn tan(self) -> SelfTangent, $\sin(x)/\cos(x)$

Example:

#![allow(unused)]
fn main() {
use ocas_domain::DoubleF64;

let x = DoubleF64::from_f64(1.0);
let e = x.exp();
// e ≈ 2.718281828...
assert!((e.hi - std::f64::consts::E).abs() < 1e-15);

let pi = DoubleF64::from_f64(std::f64::consts::PI);
let s = pi.sin();
assert!(s.hi.abs() < 1e-15); // sin(π) ≈ 0
// Output: exp(1) ≈ e, sin(π) ≈ 0
}

Operators

DoubleF64 implements Add, Sub, Mul, Div, Neg, AddAssign, SubAssign, MulAssign, DivAssign, PartialOrd, and Zero (from num_traits).

Display

Displays the hi value when lo == 0; otherwise displays the sum $hi + lo$ in 31-digit scientific notation.

See also: Domain, RealBall


AlgebraicExtension / AlgebraicNumberField / AlgebraicElement

AlgebraicExtension

Signature: pub struct AlgebraicExtension<D: Domain> { base: D, min_poly: Vec<D::Element> }

Description: The algebraic extension $D[\alpha]/(m(\alpha))$, where $m$ is a monic polynomial. When the base domain $D$ is a field and $m$ is irreducible, the quotient ring is a field:

  • AlgebraicExtension<RationalDomain> = algebraic number field $\mathbb{Q}(\alpha)$
  • AlgebraicExtension<FiniteField> = Galois field $\mathrm{GF}(p^d)$

Elements are residue classes represented by the unique polynomial representative of degree less than $\deg(m)$. Inverses use the extended Euclidean algorithm over the base domain.

⚠️ Note: irreducibility of the minimal polynomial is not checked. Under a reducible modulus the ring has zero divisors, and Domain::inv returns None for non-units.

Implements: Domain, EuclideanDomain

Construction

AlgebraicExtension::new

Signature: pub fn new(base: D, min_poly: Vec<D::Element>) -> Self

Description: Creates an algebraic extension from a base domain and a monic minimal polynomial.

Parameters:

ParameterTypeDescription
baseD: DomainThe base domain
min_polyVec<D::Element>The minimal polynomial, in ascending order, monic (leading coefficient 1), of degree $\geq 1$

Errors: debug-mode checks: (1) degree $\geq 1$ (i.e. min_poly.len() >= 2); (2) monic.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{AlgebraicExtension, Domain, Rational, RationalDomain};

// ℚ(√2): minimal polynomial α² − 2
let two = Rational::new(2, 1);
let neg_two = RationalDomain.neg(&two);
let field = AlgebraicExtension::new(
    RationalDomain,
    vec![neg_two, Rational::new(0, 1), Rational::new(1, 1)],
);
let sqrt2 = field.alpha();
// √2 · √2 = 2
assert_eq!(field.mul(&sqrt2, &sqrt2), field.from_base(two));
// Output: α² = 2
}

Accessors

AlgebraicExtension::base_domain

Signature: pub fn base_domain(&self) -> &D

Description: Returns a reference to the base domain.

AlgebraicExtension::min_poly

Signature: pub fn min_poly(&self) -> &[D::Element]

Description: Returns the minimal polynomial coefficients (ascending order, monic).

AlgebraicExtension::extension_degree

Signature: pub fn extension_degree(&self) -> usize

Description: Returns the extension degree $\deg(m)$.

Element construction

AlgebraicExtension::from_base

Signature: pub fn from_base(&self, c: D::Element) -> AlgebraicElement<D::Element>

Description: Embeds a base-domain constant into the extension.

AlgebraicExtension::alpha

Signature: pub fn alpha(&self) -> AlgebraicElement<D::Element>

Description: Returns the generator $\alpha$ of the extension.

AlgebraicExtension::element

Signature: pub fn element(&self, coeffs: Vec<D::Element>) -> AlgebraicElement<D::Element>

Description: Creates an element from coefficients (ascending order), reducing modulo the minimal polynomial automatically.

Domain implementation

  • mul: multiply first, then reduce modulo $m(\alpha)$
  • inv: extended Euclidean algorithm — if $\gcd(a, m) = 1$ (a constant), then $a^{-1} = s \bmod m$
  • div: $a/b = a \cdot b^{-1}$
  • is_zero: the coefficient vector is empty

EuclideanDomain implementation

  • div_rem: division is exact in a field; the remainder is always zero
  • gcd: degenerates in a field — 0 when both are zero, otherwise 1

Example (Galois field $\mathrm{GF}(3^2)$):

#![allow(unused)]
fn main() {
use ocas_domain::{AlgebraicExtension, Domain};
use ocas_domain::FiniteField;
use num_bigint::BigInt;

let base = FiniteField::new(BigInt::from(3));
let field = AlgebraicExtension::new(
    base.clone(),
    vec![base.element(1), base.element(0), base.element(1)], // α² + 1
);
let alpha = field.alpha();
// α² = −1 = 2 (mod 3)
assert_eq!(field.mul(&alpha, &alpha), field.from_base(base.element(2)));
// The multiplicative group has order 8: (1+α)⁸ = 1
let a = field.add(&field.one(), &alpha);
assert_eq!(field.pow(&a, 8), field.one());
// Output: α² = 2, (1+α)⁸ = 1
}

See also: Domain, Algebraic number fields


AlgebraicNumberField

Signature: pub type AlgebraicNumberField = AlgebraicExtension<RationalDomain>;

Description: Type alias for $\mathbb{Q}(\alpha)$.


AlgebraicElement

Signature: pub struct AlgebraicElement<E> { coeffs: Vec<E> }

Description: An element of an algebraic extension — a polynomial residue class in $\alpha$ of degree less than the extension degree. Coefficients are stored in ascending order with trailing zeros trimmed. The zero element has an empty coefficient vector.

Derives: Debug, Clone, PartialEq, Eq, Hash

AlgebraicElement::coeffs

Signature: pub fn coeffs(&self) -> &[E]

Description: Returns the coefficient slice (ascending order, trailing zeros trimmed).

Display

Display format: 0 (zero element), c (pure constant), (c1)·α + c0, (c2)·α^2 + (c1)·α + c0, etc. (the constant term is not parenthesized; terms are joined with +).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{AlgebraicExtension, Domain, Rational, RationalDomain};

// ℚ(i): minimal polynomial α² + 1
let field = AlgebraicNumberField::new(
    RationalDomain,
    vec![Rational::new(1, 1), Rational::new(0, 1), Rational::new(1, 1)],
);
let i = field.alpha();
let one_plus_i = field.add(&field.one(), &i);
// (1+i)⁻¹ = (1-i)/2
let inv = field.inv(&one_plus_i).unwrap();
assert_eq!(inv.coeffs(), &[Rational::new(1, 2), Rational::new(-1, 2)]);
// Output: 1/(1+i) = (1/2) - (1/2)·α
}

See also: AlgebraicExtension


Assumptions system

The assumptions system declares properties of symbolic variables (such as "x is real" or "n is a positive integer"). Solvers and simplifiers use assumptions to choose algorithms and validate solutions.

Module path: ocas_domain::assumptions


Assumption

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Assumption {
    Real, Complex, Integer, Rational,
    Positive, Negative, NonNegative, NonPositive, NonZero, Finite,
    Even, Odd, Prime,
}
}

Description: A single predicate that can be declared about a symbolic variable. Assumptions are independent — a variable may carry several assumptions at once (e.g. Positive | Integer).

Variant descriptions

VariantMeaningDisplay
Realreal number"real"
Complexcomplex number"complex"
Integerinteger"integer"
Rationalrational number"rational"
Positivestrictly positive ($> 0$)"positive"
Negativestrictly negative ($< 0$)"negative"
NonNegativenon-negative ($\geq 0$)"non-negative"
NonPositivenon-positive ($\leq 0$)"non-positive"
NonZeronon-zero"non-zero"
Finitefinite (not $\pm\infty$)"finite"
Eveneven"even"
Oddodd"odd"
Primeprime"prime"

Assumption::implied

Signature: pub fn implied(&self) -> &'static [Assumption]

Description: Returns the list of assumptions logically implied by this assumption. When an assumption is inserted, its implications propagate automatically.

Implication table:

AssumptionImplies
PositiveNonNegative, NonZero, Real
NegativeNonPositive, NonZero, Real
NonNegativeReal
NonPositiveReal
IntegerRational, Real
RationalReal
ComplexReal
EvenInteger
OddInteger
PrimeInteger, Positive
Real, NonZero, Finite(no further implications)

Assumption::conflicts

Signature: pub fn conflicts(&self) -> &'static [Assumption]

Description: Returns the list of assumptions this assumption conflicts with. A set containing both an assumption and one of its conflicts is inconsistent.

Conflict table:

AssumptionConflicts with
PositiveNegative, NonPositive
NegativePositive, NonNegative
NonNegativeNegative
NonPositivePositive
EvenOdd
OddEven

All other assumptions have no conflicts.

BitOr support

Assumption | Assumption returns Assumptions:

#![allow(unused)]
fn main() {
use ocas_domain::assumptions::{Assumption, Assumptions};

let a = Assumption::Positive | Assumption::Integer;
assert!(a.implies(Assumption::Real));
// Output: Positive | Integer implies Real
}

See also: Assumptions, SymbolAssumptions


Assumptions

Signature: pub struct Assumptions { inner: Vec<Assumption> }

Description: A set of assumptions about one symbolic variable. Stored internally as a sorted, de-duplicated vector for small-scale efficiency. Operations are closed under logical implication — inserting Positive makes NonNegative and Real available.

Derives: Debug, Clone, PartialEq, Eq, Default

Construction

Assumptions::new

Signature: pub fn new() -> Self

Description: Creates an empty assumption set.

Assumptions::single

Signature: pub fn single(a: Assumption) -> Self

Description: Creates a set containing a single assumption (and its implications).

Queries

Assumptions::len

Signature: pub fn len(&self) -> usize

Description: Returns the number of explicitly stored assumptions.

Assumptions::is_empty

Signature: pub fn is_empty(&self) -> bool

Description: Whether the set is empty.

Assumptions::contains

Signature: pub fn contains(&self, a: Assumption) -> bool

Description: Checks whether this set implies the assumption (as a direct member or via logical implication).

Assumptions::implies

Signature: pub fn implies(&self, other: Assumption) -> bool

Description: Checks whether this set logically implies other.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::assumptions::{Assumption, Assumptions};

let mut a = Assumptions::new();
a.insert(Assumption::Positive);
a.insert(Assumption::Integer);
assert!(a.contains(Assumption::Real));      // Positive implies Real
assert!(a.implies(Assumption::NonZero));    // Positive implies NonZero
assert!(!a.implies(Assumption::Even));      // does not imply Even
// Output: all assertions pass
}

Modification

Assumptions::insert

Signature: pub fn insert(&mut self, a: Assumption) -> bool

Description: Inserts the assumption and all of its logical implications. Returns false if the insertion creates a contradiction (the inconsistent assumption is still stored; check is_consistent).

Assumptions::remove

Signature: pub fn remove(&mut self, a: Assumption)

Description: Removes the assumption. Does not remove assumptions implied by it (they may still be implied by other stored assumptions).

Assumptions::is_consistent

Signature: pub fn is_consistent(&self) -> bool

Description: Checks whether the set is consistent (no conflicting assumptions).

Assumptions::iter

Signature: pub fn iter(&self) -> impl Iterator<Item = Assumption> + '_

Description: Iterates over the stored assumptions.

Operators

  • Assumption | Assumption → Assumptions
  • Assumptions | Assumption → Assumptions
  • Assumptions | Assumptions → Assumptions (union)
  • FromIterator<Assumption> → Assumptions

Example:

#![allow(unused)]
fn main() {
use ocas_domain::assumptions::{Assumption, Assumptions};

let a: Assumptions = [Assumption::Positive, Assumption::Integer].into_iter().collect();
assert!(a.is_consistent());

let b = Assumption::Positive | Assumption::Negative;
assert!(!b.is_consistent());
// Output: Positive + Integer is consistent; Positive + Negative is inconsistent
}

See also: Assumption, SymbolAssumptions


SymbolAssumptions

Signature: pub struct SymbolAssumptions { entries: Vec<(String, Assumptions)> }

Description: A mapping from symbol names to assumptions. Solvers and simplifiers use it to determine which transformations are legal. For example, $\sqrt{x^2} \to x$ is valid only when x is assumed NonNegative.

Derives: Debug, Clone, PartialEq, Eq, Default

Construction

SymbolAssumptions::new

Signature: pub fn new() -> Self

Description: Creates an empty mapping.

Queries and modification

SymbolAssumptions::set

Signature: pub fn set(&mut self, symbol: &str, assumptions: Assumptions)

Description: Sets the assumptions for a symbol (replacing any existing entry).

SymbolAssumptions::get

Signature: pub fn get(&self, symbol: &str) -> Option<&Assumptions>

Description: Gets the assumptions for a symbol.

SymbolAssumptions::remove

Signature: pub fn remove(&mut self, symbol: &str)

Description: Removes the assumptions for a symbol.

SymbolAssumptions::check

Signature: pub fn check(&self, symbol: &str, assumption: Assumption) -> bool

Description: Checks whether a symbol satisfies a particular assumption.

SymbolAssumptions::len

Signature: pub fn len(&self) -> usize

Description: Returns the number of symbols with assumptions.

SymbolAssumptions::is_empty

Signature: pub fn is_empty(&self) -> bool

Description: Whether no symbol has assumptions.

SymbolAssumptions::iter

Signature: pub fn iter(&self) -> impl Iterator<Item = &(String, Assumptions)>

Description: Iterates over all (symbol name, assumption set) pairs.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::assumptions::{Assumption, Assumptions, SymbolAssumptions};

let mut sa = SymbolAssumptions::new();
sa.set("x", Assumptions::single(Assumption::Positive));
sa.set("n", Assumption::Integer | Assumption::Positive);

assert!(sa.check("x", Assumption::Real));      // Positive implies Real
assert!(sa.check("n", Assumption::NonZero));   // Positive implies NonZero
assert!(!sa.check("x", Assumption::Integer));  // does not imply Integer
// Output: all assertions pass
}

See also: Assumptions, Assumption

Rust API Reference: Polynomials

This chapter documents oCAS's polynomial system, covering three core data structures:

  • DenseUnivariatePolynomial<D> — dense univariate polynomials
  • SparseMultivariatePolynomial<D, O> — sparse multivariate polynomials
  • RationalPolynomial<D, O> — elements of the polynomial fraction field

as well as the monomial order trait MonomialOrder and its implementations for various orders.

Module path: ocas_poly


Table of Contents


Monomial Orders

MonomialOrder trait

Signature:

#![allow(unused)]
fn main() {
pub trait MonomialOrder: Clone + PartialEq + Eq + std::fmt::Debug + Default {
    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering;
}
}

Description: Defines a total order on monomials. A polynomial's leading term, sorting, and Gröbner basis computations all depend on it.

Parameters:

ParameterTypeDescription
self&SelfThe order instance (a zero-sized type or a type with parameters)
lhs&[usize]Exponent vector of the left monomial $[\alpha_1, \alpha_2, \dots]$
rhs&[usize]Exponent vector of the right monomial

Returns: std::cmp::Ordering — Less means lhs comes before rhs in the order (i.e., lhs is larger), Greater means lhs is smaller.

Design notes: The simple orders (Lex, Grevlex, Grlex) are zero-sized types with no runtime overhead. The parameterized orders (WeightOrder, BlockOrder, MatrixOrder) store their configuration at construction time.

See also: Lex, Grevlex, Grlex, WeightOrder, BlockOrder, MatrixOrder


Lex

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Lex;
}

Description: Lexicographic order. Compares exponent vectors component by component from left to right.

Comparison rule: lhs > rhs if and only if there exists an $i$ such that $\alpha_j = \beta_j$ for all $j < i$ and $\alpha_i > \beta_i$.

Example:

#![allow(unused)]
fn main() {
use ocas_poly::sparse::{Lex, MonomialOrder};

let a = [2, 1]; // x^2 y
let b = [1, 1]; // x y
assert_eq!(Lex.cmp(&a, &b), std::cmp::Ordering::Greater);
// a is larger in Lex order (first component 2 > 1)
}

See also: MonomialOrder trait


Grlex

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Grlex;
}

Description: Graded lexicographic order. Orders by total degree descending first, then lexicographically among monomials of equal degree.

Comparison rule:

  1. The monomial with the larger total degree $\sum \alpha_i$ comes first
  2. Monomials of equal total degree are compared lexicographically

Example:

#![allow(unused)]
fn main() {
use ocas_poly::sparse::{Grlex, MonomialOrder};

let a = [2, 0]; // x^2, degree 2
let b = [1, 1]; // xy, degree 2
let c = [0, 3]; // y^3, degree 3
// c has the highest degree, so it comes first
assert_eq!(Grlex.cmp(&c, &a), std::cmp::Ordering::Less);
// a and b have the same degree; lexicographically a > b
assert_eq!(Grlex.cmp(&a, &b), std::cmp::Ordering::Greater);
}

See also: MonomialOrder trait


Grevlex

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Grevlex;
}

Description: Graded reverse lexicographic order. Orders by total degree descending first, then compares in reverse lexicographic order (starting from the last component, with the direction reversed).

Comparison rule:

  1. The monomial with the larger total degree comes first
  2. For equal total degree, compare starting from the last component, where the smaller component comes first

Properties: Grevlex typically produces the smallest intermediate matrices in Gröbner basis computations and is the default order.

Example:

#![allow(unused)]
fn main() {
use ocas_poly::sparse::{Grevlex, Lex, MonomialOrder};

let a = [2, 1];
let b = [1, 1];
assert_eq!(Lex.cmp(&a, &b), std::cmp::Ordering::Greater);
assert_eq!(Grevlex.cmp(&a, &b), std::cmp::Ordering::Less);
// Under Grevlex, a < b (reverse lexicographic order flips the direction)
}

See also: MonomialOrder trait


WeightOrder

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightOrder {
    weights: SmallVec<[i64; 4]>,
}
}

Description: Weighted order. Orders by $\sum_i w_i \cdot \alpha_i$ descending. Suitable for elimination orders that cannot be expressed with a zero-sized type.

Construction:

#![allow(unused)]
fn main() {
pub fn new(weights: SmallVec<[i64; 4]>) -> Self
pub fn from_slice(weights: &[i64]) -> Self
}

Parameters:

ParameterTypeDescription
weightsSmallVec<[i64; 4]> or &[i64]Weight of each variable; the length should equal the number of variables

Default: all weights set to 1 (i.e., the total degree order).

Example:

#![allow(unused)]
fn main() {
use ocas_poly::sparse::{MonomialOrder, WeightOrder};
use smallvec::smallvec;

let ord = WeightOrder::new(smallvec![2, 1]);
// [1,0] → weight 2, [0,1] → weight 1 → [1,0] is larger
assert_eq!(ord.cmp(&[1, 0], &[0, 1]), std::cmp::Ordering::Less);
}

See also: MonomialOrder trait


BlockOrder and SubOrder

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockOrder {
    boundaries: SmallVec<[usize; 4]>,
    orders: SmallVec<[SubOrder; 4]>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubOrder {
    Lex,
    Grevlex,
    Grlex,
}
}

Description: Block elimination order. Partitions the variables into consecutive blocks, each block using its own sub-order.

Construction:

#![allow(unused)]
fn main() {
pub fn new(boundaries: SmallVec<[usize; 4]>, orders: SmallVec<[SubOrder; 4]>) -> Self
}

Parameters:

ParameterTypeDescription
boundariesSmallVec<[usize; 4]>List of split points for the ordering (exclusive upper bounds, not including n_vars)
ordersSmallVec<[SubOrder; 4]>Sub-order for each block; its length must equal boundaries.len() + 1

Notes: boundaries = [2] with orders = [Lex, Grevlex] on a polynomial in 4 variables means: compare variables 0–1 first (Lex), and if equal, compare variables 2–3 (Grevlex).

Default: a single Grevlex block.

Example:

#![allow(unused)]
fn main() {
use ocas_poly::sparse::{BlockOrder, MonomialOrder, SubOrder};
use smallvec::smallvec;

let ord = BlockOrder::new(
    smallvec![2],
    smallvec![SubOrder::Lex, SubOrder::Grevlex],
);
let a = [1, 0, 0, 0]; // x₀
let b = [0, 1, 0, 0]; // x₁
// Lex within the block: [1,0] > [0,1]
assert_eq!(ord.cmp(&a, &b), std::cmp::Ordering::Greater);
}

See also: MonomialOrder trait


MatrixOrder

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatrixOrder {
    matrix: Vec<Vec<i64>>,
    n_vars: usize,
}
}

Description: Matrix order. Multiplies exponent vectors by an integer matrix and compares the results lexicographically. Given an $n \times n$ matrix $M$, a monomial $\alpha > \beta$ if and only if $M\alpha >_{\text{lex}} M\beta$. It generalizes all standard orders and is especially suitable for constructing elimination orders.

Construction:

#![allow(unused)]
fn main() {
pub fn new(matrix: Vec<Vec<i64>>) -> Self
pub fn elimination_order(elim_vars: usize, n_vars: usize) -> Self
}

Parameters:

ParameterTypeDescription
matrixVec<Vec<i64>>$n \times n$ weight matrix (row-major)
elim_varsusizeNumber of leading variables to eliminate
n_varsusizeTotal number of variables

elimination_order: Constructs a matrix order equivalent to BlockOrder([elim_vars in Lex, rest in Grevlex]).

Default: the $1 \times 1$ identity matrix.

Example:

#![allow(unused)]
fn main() {
use ocas_poly::sparse::{MatrixOrder, MonomialOrder};

// 2×2 identity matrix = Lex order
let ord = MatrixOrder::new(vec![vec![1, 0], vec![0, 1]]);
assert_eq!(ord.cmp(&[1, 0], &[0, 1]), std::cmp::Ordering::Greater);
}

See also: MonomialOrder trait


DenseUnivariatePolynomial

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DenseUnivariatePolynomial<D: Domain> {
    coeffs: Vec<D::Element>,  // starts at the constant term, trailing zeros removed
    domain: D,
}
}

A dense univariate polynomial. Coefficients are stored in a contiguous vector, from the constant term $a_0$ to the highest-degree term $a_n$. The zero polynomial is represented by an empty vector. Multiplication automatically selects between Karatsuba (when both polynomials have at least 32 coefficients) and schoolbook multiplication.


Dense Construction and Properties

DenseUnivariatePolynomial::new

Signature: pub fn new(domain: D) -> Self

Description: Creates the zero polynomial.

Parameters:

ParameterTypeDescription
domainDThe coefficient domain

Returns: the zero polynomial (an empty coefficient vector).

See also: from_coeffs


DenseUnivariatePolynomial::from_coeffs

Signature: pub fn from_coeffs(domain: D, coeffs: Vec<D::Element>) -> Self

Description: Constructs a polynomial from a coefficient vector. Trailing zero coefficients are removed automatically.

Parameters:

ParameterTypeDescription
domainDThe coefficient domain
coeffsVec<D::Element>Coefficients $[a_0, a_1, \dots, a_n]$, constant term first

Returns: the polynomial $a_0 + a_1 x + \cdots + a_n x^n$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let domain = IntegerDomain;
let p = DenseUnivariatePolynomial::from_coeffs(
    domain,
    vec![Integer::from(1), Integer::from(0), Integer::from(2)],
);
assert_eq!(p.degree(), Some(2));
assert_eq!(p.coeff(2), Some(&Integer::from(2)));
// p(x) = 1 + 2x^2, the middle zero coefficient is kept (not a trailing zero)
}

See also: new


DenseUnivariatePolynomial::domain

Signature: pub fn domain(&self) -> &D

Description: Returns a reference to the coefficient domain.


DenseUnivariatePolynomial::coeffs

Signature: pub fn coeffs(&self) -> &[D::Element]

Description: Returns the coefficient slice, starting from the constant term.


DenseUnivariatePolynomial::is_zero

Signature: pub fn is_zero(&self) -> bool

Description: Returns whether the polynomial is zero.


DenseUnivariatePolynomial::degree

Signature: pub fn degree(&self) -> Option<usize>

Description: Returns the degree of the polynomial. Returns None for the zero polynomial.


DenseUnivariatePolynomial::coeff

Signature: pub fn coeff(&self, n: usize) -> Option<&D::Element>

Description: Returns the coefficient of $x^n$, or None if it does not exist.


DenseUnivariatePolynomial::leading_coeff

Signature: pub fn leading_coeff(&self) -> Option<&D::Element>

Description: Returns the leading coefficient. Returns None for the zero polynomial.


DenseUnivariatePolynomial::lcoeff

Signature: pub fn lcoeff(&self) -> D::Element

Description: Convenience alias for the leading coefficient. Returns the domain's zero for the zero polynomial.

Returns: the leading coefficient, or the domain's zero.

See also: leading_coeff


DenseUnivariatePolynomial::constant

Signature: pub fn constant(&self) -> D::Element

Description: Returns the constant term (the coefficient of $x^0$). Returns the domain's zero for the zero polynomial.


DenseUnivariatePolynomial::zero

Signature: pub fn zero(&self) -> Self

Description: Returns the zero polynomial over the same domain.


DenseUnivariatePolynomial::one

Signature: pub fn one(&self) -> Self

Description: Returns the constant polynomial $1$.


DenseUnivariatePolynomial::is_one

Signature: pub fn is_one(&self) -> bool

Description: Returns whether the polynomial is the constant $1$.


Dense Basic Arithmetic

The following methods are available for D: Domain:

DenseUnivariatePolynomial::neg

Signature: pub fn neg(&self) -> Self

Description: Returns $-p(x)$.


DenseUnivariatePolynomial::add

Signature: pub fn add(&self, other: &Self) -> Self

Description: Polynomial addition.


DenseUnivariatePolynomial::sub

Signature: pub fn sub(&self, other: &Self) -> Self

Description: Polynomial subtraction.


DenseUnivariatePolynomial::mul_scalar

Signature: pub fn mul_scalar(&self, scalar: &D::Element) -> Self

Description: Scalar multiplication. Multiplies every coefficient by scalar.


DenseUnivariatePolynomial::mul

Signature: pub fn mul(&self, other: &Self) -> Self

Description: Polynomial multiplication. Automatically selects between schoolbook multiplication and Karatsuba based on the degree.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let domain = IntegerDomain;
let a = DenseUnivariatePolynomial::from_coeffs(
    domain,
    vec![Integer::from(1), Integer::from(1)],
);
let b = DenseUnivariatePolynomial::from_coeffs(
    domain,
    vec![Integer::from(1), Integer::from(-1)],
);
let c = a.mul(&b);
assert_eq!(c.coeffs(), &[
    Integer::from(1),
    Integer::from(0),
    Integer::from(-1),
]);
// (1 + x)(1 - x) = 1 - x^2
}

See also: mul_into


DenseUnivariatePolynomial::mul_into

Signature: pub fn mul_into(&self, other: &Self, buf: &mut Vec<D::Element>)

Description: Multiplication that writes the result into a buffer (avoiding repeated heap allocations in hot loops). The buffer is cleared and reused.

Parameters:

ParameterTypeDescription
other&SelfThe other polynomial
buf&mut Vec<D::Element>Output buffer; after the call it contains the product coefficients

DenseUnivariatePolynomial::eval

Signature: pub fn eval(&self, x: &D::Element) -> D::Element

Description: Evaluates the polynomial at $x$ using Horner's method. Returns the domain's zero for the zero polynomial.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let domain = IntegerDomain;
let p = DenseUnivariatePolynomial::from_coeffs(
    domain,
    vec![Integer::from(1), Integer::from(2), Integer::from(3)],
);
let value = p.eval(&Integer::from(2));
assert_eq!(value, Integer::from(17));
// p(2) = 1 + 2·2 + 3·4 = 17
}

DenseUnivariatePolynomial::derivative

Signature: pub fn derivative(&self) -> Self

Description: Returns the formal derivative $p'(x)$.


DenseUnivariatePolynomial::integral

Signature: pub fn integral(&self) -> Self

Description: Returns the formal integral $\int p(x),dx$ with zero constant term. Requires a domain that supports division (such as RationalDomain).


EuclideanDomain Operations

The following methods require D: EuclideanDomain:

DenseUnivariatePolynomial::mul_coeff

Signature: pub fn mul_coeff(&self, c: &D::Element) -> Self

Description: Multiplies every coefficient by the constant $c$. Equivalent to mul_scalar, but restricted to EuclideanDomain.

See also: mul_scalar


DenseUnivariatePolynomial::div_coeff

Signature: pub fn div_coeff(&self, c: &D::Element) -> Self

Description: Divides every coefficient by the constant $c$ (which must divide exactly).

Errors: panics unconditionally if $c$ is not invertible in the domain (e.g. over $\mathbb{Z}$ when $c$ is not $\pm 1$), even if every coefficient is divisible by $c$.


DenseUnivariatePolynomial::div_rem

Signature: pub fn div_rem(&self, divisor: &Self) -> Option<(Self, Self)>

Description: Division with remainder. Returns $(q, r)$ such that $p = q \cdot \text{divisor} + r$, where $\deg(r) < \deg(\text{divisor})$.

Returns: Some((quotient, remainder)), or None if the divisor is zero.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let domain = IntegerDomain;
let p = DenseUnivariatePolynomial::from_coeffs(
    domain,
    vec![Integer::from(1), Integer::from(0), Integer::from(-1)],
);
let q = DenseUnivariatePolynomial::from_coeffs(
    domain,
    vec![Integer::from(1), Integer::from(1)],
);
let (quot, rem) = p.div_rem(&q).unwrap();
assert_eq!(quot.coeffs(), &[Integer::from(1), Integer::from(-1)]);
assert!(rem.is_zero());
// (x^2 - 1) / (x + 1) = x - 1, remainder 0
}

See also: gcd


DenseUnivariatePolynomial::gcd

Signature: pub fn gcd(&self, other: &Self) -> Self

Description: Computes the greatest common divisor of two polynomials. Uses the Euclidean algorithm (with pseudo-remainders when the coefficient domain is not a field). The result is always primitive (content-free).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(0), Integer::from(1),
]); // x^2 - 1
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(1), Integer::from(2), Integer::from(1),
]); // (x+1)^2
let g = a.gcd(&b);
assert_eq!(g.coeffs(), &[Integer::from(1), Integer::from(1)]);
// gcd = x + 1
}

See also: extended_gcd_poly


DenseUnivariatePolynomial::content

Signature: pub fn content(&self) -> D::Element

Description: Returns the greatest common divisor of all coefficients. Returns the domain's zero for the zero polynomial.

Returns: the GCD of the coefficients.

See also: primitive_part


DenseUnivariatePolynomial::primitive_part

Signature: pub fn primitive_part(&self) -> Self

Description: Returns the primitive part $p / \text{content}(p)$. The result has content 1 (or is the zero polynomial).

See also: content


DenseUnivariatePolynomial::extended_gcd_poly

Signature: pub fn extended_gcd_poly(&self, other: &Self) -> (Self, Self, Self)

Description: Extended Euclidean algorithm. Returns $(g, s, t)$ such that $s \cdot p + t \cdot q = g$, where $g = \gcd(p, q)$ and $g$ is monic.

Returns: (gcd, bezout_s, bezout_t)

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::DenseUnivariatePolynomial;

let d = RationalDomain;
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Rational::new(1, 1), Rational::new(0, 1), Rational::new(1, 1),
]); // x^2 + 1
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Rational::new(1, 1), Rational::new(1, 1),
]); // x + 1
let (g, s, t) = a.extended_gcd_poly(&b);
// s·a + t·b = g
}

See also: gcd


DenseUnivariatePolynomial::pow

Signature: pub fn pow(&self, n: u32) -> Self

Description: Fast exponentiation by repeated squaring. $p^0 = 1$.

Parameters:

ParameterTypeDescription
nu32Non-negative integer exponent

Returns: $p(x)^n$.


DenseUnivariatePolynomial::p_adic_expansion

Signature: pub fn p_adic_expansion(&self, p: &Self) -> Vec<Self>

Description: $p$-adic expansion. Returns $[a_0, a_1, a_2, \dots]$ such that $\text{self} = a_0 + a_1 \cdot p + a_2 \cdot p^2 + \cdots$, where each $a_k$ has degree less than $\deg(p)$.

Implementation: by repeated division with remainder.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// p-adic expansion of f(x) = x^3 with respect to p(x) = x + 1
let f = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(0), Integer::from(0), Integer::from(0), Integer::from(1),
]);
let p = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(1), Integer::from(1),
]);
let expansion = f.p_adic_expansion(&p);
// expansion = [a0, a1, a2, ...] such that f = a0 + a1*p + a2*p^2 + ...
}

See also: div_rem


DenseUnivariatePolynomial::diophantine

Signature: pub fn diophantine(polys: &mut [Self], b: &Self) -> Vec<Self>

Description: Polynomial CRT (Diophantine solver). Given a list of pairwise coprime polynomials polys and a target $b$, returns $[s_0, \dots, s_n]$ such that:

$$\sum_i s_i \cdot \prod_{j \neq i} p_j \equiv b \pmod{\prod_i p_i}$$

Parameters:

ParameterTypeDescription
polys&mut [Self]List of pairwise coprime polynomials
b&SelfThe target polynomial

Returns: the list of remainders $[s_0, \dots, s_n]$.

Errors: panics if the polynomials are not pairwise coprime.


Factorization and Resultants

DenseUnivariatePolynomial::square_free_factorization

Signature: pub fn square_free_factorization(&self) -> SquareFreeFactors<D>

Description: Square-free factorization. Uses Yun's algorithm: $g = \gcd(f, f')$, $w = f/g$, then iterate $h = \gcd(w, g)$, $z = w/h$ to collect the factors of multiplicity $k$.

Returns: Vec<(DenseUnivariatePolynomial<D>, usize)> — a list of (factor, multiplicity) pairs.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// (x+1)^2*(x-1) = x^3 + x^2 - x - 1
let p = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(-1), Integer::from(1), Integer::from(1),
]);
let factors = p.square_free_factorization();
assert_eq!(factors.len(), 2);
}

See also: factor, gcd


DenseUnivariatePolynomial::is_square_free

Signature: pub fn is_square_free(&self) -> bool

Description: Returns whether the polynomial is square-free ($\gcd(p, p') = 1$).


DenseUnivariatePolynomial::factor (IntegerDomain)

Signature: impl DenseUnivariatePolynomial<IntegerDomain> { pub fn factor(&self) -> Factors<IntegerDomain> }

Description: Completely factors a primitive integer polynomial into irreducible factors. Uses the square-free factorization + Berlekamp–Zassenhaus + Hensel lifting algorithms.

Preconditions: the input must be primitive (content = 1). Call primitive_part() first on arbitrary polynomials.

Returns: Vec<(DenseUnivariatePolynomial<IntegerDomain>, usize)> — (irreducible factor, multiplicity) pairs.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// x^2 - 1 = (x-1)(x+1)
let p = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(0), Integer::from(1),
]);
let factors = p.factor();
assert_eq!(factors.len(), 2);
}

See also: square_free_factorization


DenseUnivariatePolynomial::factor (FiniteField)

Signature: impl DenseUnivariatePolynomial<FiniteField> { pub fn factor(&self) -> Factors<FiniteField> }

Description: Completely factors a polynomial over $\mathbb{F}_p$. Uses Berlekamp's algorithm (or Cantor–Zassenhaus).

Returns: Vec<(DenseUnivariatePolynomial<FiniteField>, usize)> — (irreducible factor, multiplicity) pairs.

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::{Domain, FiniteField};
use ocas_poly::DenseUnivariatePolynomial;

let f = FiniteField::new(BigInt::from(5));
// x^2 - 1 over F_5
let p = DenseUnivariatePolynomial::from_coeffs(
    f.clone(), vec![f.element(4), f.element(0), f.element(1)]);
let factors = p.factor();
assert!(!factors.is_empty());
}

See also: factor (IntegerDomain)


DenseUnivariatePolynomial::resultant

Signature: pub fn resultant(&self, other: &Self) -> D::Element

Description: Computes the resultant $\operatorname{Res}(a, b)$ using the Brown PRS algorithm. The resultant is zero if and only if $a$ and $b$ have a non-constant common factor.

Implementation details: subresultant PRS, with an exact division by $\beta$ at each step (the subresultant theorem guarantees exactness).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// Res(x - 1, x - 2) = 1 - 2 = -1
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(1),
]);
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-2), Integer::from(1),
]);
assert_eq!(a.resultant(&b), Integer::from(-1));
}

Properties: $\operatorname{Res}(a, b) = (-1)^{\deg a \cdot \deg b} \operatorname{Res}(b, a)$.

See also: gcd


SparseMultivariatePolynomial

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SparseMultivariatePolynomial<D: Domain, O: MonomialOrder = Grevlex> {
    terms: HashMap<SmallVec<[usize; 4]>, D::Element>,
    domain: D,
    n_vars: usize,
    pub order: O,
}
}

A sparse multivariate polynomial. Only nonzero terms are stored, in a HashMap keyed by the exponent vector $\vec{e} = [e_1, e_2, \dots]$ with the coefficient as value. An exponent vector $\vec{e}$ represents the monomial $x_1^{e_1} x_2^{e_2} \cdots$. The monomial order is controlled by the type parameter O, which defaults to Grevlex.


Sparse Construction and Properties

SparseMultivariatePolynomial::new

Signature: pub fn new(domain: D, n_vars: usize) -> Self

Description: Creates the zero polynomial with the default monomial order.

Parameters:

ParameterTypeDescription
domainDThe coefficient domain
n_varsusizeNumber of variables

See also: new_with_order


SparseMultivariatePolynomial::new_with_order

Signature: pub fn new_with_order(domain: D, n_vars: usize, order: O) -> Self

Description: Creates the zero polynomial with the given monomial order.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::IntegerDomain;
use ocas_poly::sparse::{SparseMultivariatePolynomial, WeightOrder};

let order = WeightOrder::from_slice(&[2, 1]);
let p = SparseMultivariatePolynomial::<_, WeightOrder>::new_with_order(
    IntegerDomain, 2, order,
);
assert_eq!(p.n_vars(), 2);
}

SparseMultivariatePolynomial::from_terms

Signature: pub fn from_terms(domain: D, n_vars: usize, terms: Vec<(Vec<usize>, D::Element)>) -> Self

Description: Constructs a polynomial from a list of (exponent vector, coefficient) pairs. Terms with zero coefficients are dropped automatically.

Parameters:

ParameterTypeDescription
domainDThe coefficient domain
n_varsusizeNumber of variables
termsVec<(Vec<usize>, D::Element)>(exponent vector, coefficient) pairs

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::sparse::Grevlex;
use ocas_poly::SparseMultivariatePolynomial;

let domain = IntegerDomain;
let p = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
    domain,
    2,
    vec![(vec![1, 0], Integer::from(2)), (vec![0, 1], Integer::from(3))],
);
assert_eq!(p.n_terms(), 2);
assert_eq!(p.coeff(&[1, 0]), Integer::from(2));
// p = 2x + 3y
}

See also: new


SparseMultivariatePolynomial::domain

Signature: pub fn domain(&self) -> &D

Description: Returns a reference to the coefficient domain.


SparseMultivariatePolynomial::n_vars

Signature: pub fn n_vars(&self) -> usize

Description: Returns the number of variables.


SparseMultivariatePolynomial::n_terms

Signature: pub fn n_terms(&self) -> usize

Description: Returns the number of nonzero terms.


SparseMultivariatePolynomial::is_zero

Signature: pub fn is_zero(&self) -> bool

Description: Returns whether the polynomial is zero.


SparseMultivariatePolynomial::terms_ref

Signature: pub fn terms_ref(&self) -> &HashMap<SmallVec<[usize; 4]>, D::Element>

Description: Returns a reference to the internal term map (exponent → coefficient).


SparseMultivariatePolynomial::set_term_external

Signature: pub fn set_term_external(&mut self, exp: Vec<usize>, coeff: D::Element)

Description: Sets the coefficient of a monomial. A zero coefficient removes the term.


SparseMultivariatePolynomial::total_degree

Signature: pub fn total_degree(&self) -> Option<usize>

Description: Returns the total degree (the maximum degree over all monomials). Returns None for the zero polynomial.


SparseMultivariatePolynomial::coeff

Signature: pub fn coeff(&self, exp: &[usize]) -> D::Element

Description: Returns the coefficient of the given monomial, or the domain's zero if it is absent.


SparseMultivariatePolynomial::degree_in

Signature: pub fn degree_in(&self, var_index: usize) -> usize

Description: Returns the degree in the variable var_index. Returns 0 for the zero polynomial.

Parameters:

ParameterTypeDescription
var_indexusizeVariable index (0-based)

Returns: the maximum exponent of that variable over all terms.


SparseMultivariatePolynomial::zero / one

Signature:

#![allow(unused)]
fn main() {
pub fn zero(&self) -> Self
pub fn one(&self) -> Self
}

Description: Returns the zero polynomial or the constant $1$ with the same shape.


Sparse Basic Arithmetic

SparseMultivariatePolynomial::neg

Signature: pub fn neg(&self) -> Self

Description: Returns $-p$.


SparseMultivariatePolynomial::add

Signature: pub fn add(&self, other: &Self) -> Self

Description: Polynomial addition.

Errors: panics if the numbers of variables differ.


SparseMultivariatePolynomial::sub

Signature: pub fn sub(&self, other: &Self) -> Self

Description: Polynomial subtraction.

Errors: panics if the numbers of variables differ.


SparseMultivariatePolynomial::mul_scalar

Signature: pub fn mul_scalar(&self, scalar: &D::Element) -> Self

Description: Scalar multiplication.


SparseMultivariatePolynomial::mul

Signature: pub fn mul(&self, other: &Self) -> Self

Description: Polynomial multiplication. Multiplies each pair of terms and combines like terms.

Errors: panics if the numbers of variables differ.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::sparse::Grevlex;
use ocas_poly::SparseMultivariatePolynomial;

let domain = IntegerDomain;
let p = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
    domain,
    2,
    vec![(vec![1, 0], Integer::from(2)), (vec![0, 1], Integer::from(3))],
);
let q = SparseMultivariatePolynomial::<IntegerDomain, Grevlex>::from_terms(
    domain,
    2,
    vec![(vec![1, 0], Integer::from(1)), (vec![0, 0], Integer::from(1))],
);
let r = p.mul(&q);
assert_eq!(r.coeff(&[1, 0]), Integer::from(2));
assert_eq!(r.coeff(&[0, 1]), Integer::from(3));
assert_eq!(r.coeff(&[2, 0]), Integer::from(2));
// (2x + 3y)(x + 1) = 2x^2 + 2x + 3xy + 3y
}

SparseMultivariatePolynomial::mul_monomial

Signature: pub fn mul_monomial(&self, exp: &[usize]) -> Self

Description: Adds exp component-wise to the exponent vector of every term. Used in Gröbner basis reduction.


SparseMultivariatePolynomial::sorted_terms

Signature: pub fn sorted_terms(&self) -> Vec<(&SmallVec<[usize; 4]>, &D::Element)>

Description: Returns the (exponent, coefficient) pairs sorted by the monomial order.


SparseMultivariatePolynomial::eval

Signature: pub fn eval(&self, var_index: usize, value: &D::Element) -> Self

Description: Substitutes the value value for the variable var_index, returning a polynomial in one fewer variable.

Parameters:

ParameterTypeDescription
var_indexusizeIndex of the variable to substitute
value&D::ElementThe substitution value

Returns: a polynomial in one fewer variable (the remaining variables keep their relative order).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::Lex;

let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
    IntegerDomain, 2,
    vec![
        (vec![1, 1], Integer::from(1)), // xy
        (vec![0, 1], Integer::from(2)), // 2y
    ],
);
// Substitute x=3: the result is 3y + 2y = 5y
let r = p.eval(0, &Integer::from(3));
assert_eq!(r.coeff(&[1]), Integer::from(5));
}

SparseMultivariatePolynomial::eval_keep

Signature: pub fn eval_keep(&self, var_index: usize, value: &D::Element) -> Self

Description: Substitutes the variable var_index with a value but keeps the total number of variables unchanged (the substituted variable's exponents are zeroed). Used in scenarios where variable positions must stay fixed during Hensel lifting.


SparseMultivariatePolynomial::leading_term

Signature: pub fn leading_term(&self) -> Option<(&SmallVec<[usize; 4]>, &D::Element)>

Description: Returns the leading term (exponent vector, coefficient). Returns None for the zero polynomial. An $O(n)$ scan of the HashMap.


SparseMultivariatePolynomial::leading_monomial

Signature: pub fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>>

Description: Returns the exponent vector of the leading monomial.


SparseMultivariatePolynomial::leading_coeff

Signature: pub fn leading_coeff(&self) -> Option<&D::Element>

Description: Returns the leading coefficient.


SparseMultivariatePolynomial::content

Signature:

#![allow(unused)]
fn main() {
pub fn content(&self) -> D::Element
where D: EuclideanDomain
}

Description: Returns the GCD of all coefficients.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::Lex;

let p = SparseMultivariatePolynomial::<_, Lex>::from_terms(
    IntegerDomain, 1,
    vec![(vec![2], Integer::from(6)), (vec![1], Integer::from(9)), (vec![0], Integer::from(3))],
);
assert_eq!(p.content(), Integer::from(3));
}

See also: primitive_part


SparseMultivariatePolynomial::primitive_part

Signature:

#![allow(unused)]
fn main() {
pub fn primitive_part(&self) -> Self
where D: EuclideanDomain
}

Description: Returns the primitive part (polynomial / content).

See also: content


SparseMultivariatePolynomial::div_exact

Signature: pub fn div_exact(&self, divisor: &Self) -> Self

Description: Exact division (assumes no remainder). Used in rational function normalization where the GCD is known to divide.

Errors: panics in debug mode if the division is not exact.

See also: checked_div_exact


SparseMultivariatePolynomial::checked_div_exact

Signature: pub fn checked_div_exact(&self, divisor: &Self) -> Option<Self>

Description: Exact division, returning None when the division is not exact.

Returns: Some(quotient) or None.


SparseMultivariatePolynomial::derivative

Signature: pub fn derivative(&self, var_index: usize) -> Self

Description: Computes the formal partial derivative with respect to the variable var_index.


SparseMultivariatePolynomial::taylor_coefficients

Signature: pub fn taylor_coefficients(&self, var_index: usize, a: &D::Element) -> Vec<Self>

Description: Computes the Taylor coefficients in the variable var_index at the point $a$. Returns $[t_0, t_1, \dots, t_d]$ such that $f = \sum_j t_j (x_{\text{var}} - a)^j$.


Gröbner Basis Support

SparseMultivariatePolynomial::reduce

Signature: pub fn reduce(&self, basis: &[Self]) -> Self

Description: Multivariate polynomial division. Repeatedly finds a basis element whose leading term divides the current leading term and subtracts the appropriate multiple; otherwise the leading term is moved into the remainder. Requires the coefficient domain to be a field (division always succeeds).

Parameters:

ParameterTypeDescription
basis&[Self]The list of divisors

Returns: the reduced remainder.


SparseMultivariatePolynomial::spoly

Signature: pub fn spoly(&self, other: &Self) -> Self

Description: Computes the S-polynomial. $S(f, g) = \frac{\text{lcm}}{\text{lt}(f)} \cdot f - \frac{\text{lcm}}{\text{lt}(g)} \cdot g$.


SparseMultivariatePolynomial::make_monic_inplace

Signature: pub fn make_monic_inplace(&mut self) -> bool

Description: Normalizes the polynomial to be monic in place (divides by the leading coefficient). Returns false if the leading coefficient is not invertible.


SparseMultivariatePolynomial::exponents_iter

Signature: pub fn exponents_iter(&self) -> impl Iterator<Item = &SmallVec<[usize; 4]>>

Description: Iterates over all exponent vectors in monomial order (descending). Used for the symbolic preprocessing of the F4 algorithm.


Multivariate Factorization

SparseMultivariatePolynomial::factor (IntegerDomain, Lex)

Signature:

#![allow(unused)]
fn main() {
impl SparseMultivariatePolynomial<IntegerDomain, Lex> {
    pub fn factor(&self) -> Vec<(Self, usize)>
}
}

Description: Factors a multivariate integer polynomial into irreducible factors. Bivariate polynomials with a constant leading coefficient in the main variable use bivariate Hensel lifting (Wang); polynomials in three or more variables, or bivariate ones with a non-constant leading coefficient, use the EEZ algorithm.

Returns: a list of (irreducible factor, multiplicity) pairs.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::Lex;

// (x^2 + y + 1)(x + y + 2)
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
    IntegerDomain, 2,
    vec![
        (vec![3, 0], Integer::from(1)),
        (vec![2, 1], Integer::from(1)),
        (vec![2, 0], Integer::from(2)),
        (vec![1, 1], Integer::from(1)),
        (vec![1, 0], Integer::from(1)),
        (vec![0, 2], Integer::from(1)),
        (vec![0, 1], Integer::from(3)),
        (vec![0, 0], Integer::from(2)),
    ],
);
let factors = f.factor();
assert!(factors.len() >= 2);
}

See also: factor (FiniteField)


SparseMultivariatePolynomial::factor (FiniteField, Lex)

Signature:

#![allow(unused)]
fn main() {
impl SparseMultivariatePolynomial<FiniteField, Lex> {
    pub fn factor(&self) -> Vec<(Self, usize)>
}
}

Description: Factors a multivariate $\mathbb{F}_p$ polynomial. Bivariate polynomials use the evaluation–Hensel path; three or more variables use EEZ.

See also: factor (IntegerDomain)


RationalPolynomial

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RationalPolynomial<D: Domain, O: MonomialOrder = Grevlex> {
    pub numerator: SparseMultivariatePolynomial<D, O>,
    pub denominator: SparseMultivariatePolynomial<D, O>,
}
}

An element of the polynomial fraction field $\frac{\text{num}}{\text{den}}$. Constructing via from_num_den automatically normalizes to lowest terms (coprime numerator and denominator, with the denominator's leading coefficient normalized).


RationalPolynomial::new

Signature:

#![allow(unused)]
fn main() {
pub fn new(
    numerator: SparseMultivariatePolynomial<D, O>,
    denominator: SparseMultivariatePolynomial<D, O>,
) -> Self
}

Description: Constructs a rational polynomial without normalization. The caller must ensure the denominator is nonzero.

See also: from_num_den


RationalPolynomial::from_num_den

Signature:

#![allow(unused)]
fn main() {
impl<D: EuclideanDomain, O: MonomialOrder> RationalPolynomial<D, O> {
    pub fn from_num_den(
        numerator: SparseMultivariatePolynomial<D, O>,
        denominator: SparseMultivariatePolynomial<D, O>,
    ) -> Self
}
}

Description: Constructs from numerator and denominator and normalizes. The result is in lowest terms: coprime numerator and denominator, with the denominator's leading coefficient normalized (1 over a finite field, positive over the integers).

Preconditions: the denominator must be nonzero.

Normalization steps:

  1. Compute $\gcd(\text{num}, \text{den})$ and cancel it
  2. In the univariate case, reduce by an exact GCD via the dense Euclidean algorithm
  3. Normalize the denominator's leading coefficient

Errors: panics when the denominator is zero.

See also: new


RationalPolynomial::from_polynomial

Signature: pub fn from_polynomial(poly: SparseMultivariatePolynomial<D, O>) -> Self

Description: Constructs from a polynomial (denominator = 1).


RationalPolynomial::zero / one

Signature:

#![allow(unused)]
fn main() {
pub fn zero(domain: &D, n_vars: usize) -> Self
pub fn one(domain: &D, n_vars: usize) -> Self
}

Description: Returns the zero or the identity rational polynomial.


RationalPolynomial::is_zero / is_one

Signature:

#![allow(unused)]
fn main() {
pub fn is_zero(&self) -> bool
pub fn is_one(&self) -> bool
}

Description: Returns whether the rational polynomial is zero or $1/1$.


RationalPolynomial::n_vars

Signature: pub fn n_vars(&self) -> usize

Description: Returns the number of variables.


RationalPolynomial::domain

Signature: pub fn domain(&self) -> &D

Description: Returns a reference to the coefficient domain.


RationalPolynomial::neg

Signature: pub fn neg(&self) -> Self

Description: Returns $-\frac{n}{d}$.


RationalPolynomial::inv

Signature: pub fn inv(&self) -> Option<Self>

Description: Returns the multiplicative inverse $\frac{d}{n}$. Returns None when the numerator is zero.

Returns: Some(inverse) or None (when the numerator is zero).


RationalPolynomial::pow

Signature: pub fn pow(&self, k: u32) -> Self

Description: Fast exponentiation. Computes $\left(\frac{n}{d}\right)^k$ by repeated squaring of the numerator and denominator separately.


RationalPolynomial::add

Signature:

#![allow(unused)]
fn main() {
impl<D: EuclideanDomain, O: MonomialOrder> RationalPolynomial<D, O> {
    pub fn add(&self, other: &Self) -> Self
}
}

Description: Addition of rational polynomials. With equal denominators the numerators are added directly; with different denominators it cross-multiplies and normalizes.


RationalPolynomial::sub

Signature: pub fn sub(&self, other: &Self) -> Self

Description: Subtraction of rational polynomials. Equivalent to self.add(&other.neg()).


RationalPolynomial::mul

Signature: pub fn mul(&self, other: &Self) -> Self

Description: Multiplication of rational polynomials. Cross-multiplies and normalizes.


RationalPolynomial::div

Signature: pub fn div(&self, other: &Self) -> Option<Self>

Description: Division of rational polynomials. $\frac{a/b}{c/d} = \frac{ad}{bc}$.

Returns: Some(quotient) or None (when the numerator of the divisor is zero).


Helper Functions

The following functions live in the ocas_poly::sparse module:

monomial_divides

Signature: pub fn monomial_divides(a: &[usize], b: &[usize]) -> bool

Description: Checks whether the monomial $b$ divides $a$ (i.e., $a$ is a multiple of $b$). Returns true iff $a_i \geq b_i$ for all $i$.

Parameters:

ParameterTypeDescription
a&[usize]Exponent vector of the dividend (the multiple)
b&[usize]Exponent vector of the divisor

Returns: true if $b$ divides $a$.


monomial_lcm

Signature: pub fn monomial_lcm(a: &[usize], b: &[usize]) -> SmallVec<[usize; 4]>

Description: Computes the least common multiple of two monomials: the component-wise maximum.

$$\text{lcm}(x^a y^b, x^c y^d) = x^{\max(a,c)} y^{\max(b,d)}$$


monomial_are_coprime

Signature: pub fn monomial_are_coprime(a: &[usize], b: &[usize]) -> bool

Description: Checks whether two monomials are coprime (no variable appears in both).


Type Aliases

#![allow(unused)]
fn main() {
/// Square-free factorization result: list of (factor, multiplicity) pairs.
pub type SquareFreeFactors<D> = Vec<(DenseUnivariatePolynomial<D>, usize)>;

/// Complete factorization result: list of (irreducible factor, multiplicity) pairs.
pub type Factors<D> = Vec<(DenseUnivariatePolynomial<D>, usize)>;
}

Matrices

Source: ocas-poly/src/matrix.rs

Matrix<D> is the general dense matrix type in oCAS, with elements from an arbitrary EuclideanDomain. It uses row-major storage, and all elimination operations use fraction-free algorithms to avoid fraction blow-up of domain elements.


MatrixError

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatrixError {
    ShapeMismatch,
    RightHandSideIsNotVector,
    Inconsistent,
    Underdetermined { rank: usize },
    ResultNotInDomain,
}
}

Errors that may arise in matrix operations. Implements Display and std::error::Error.

ShapeMismatch

Meaning: Matrix shapes are incompatible.

Triggered when: trace is used on a non-square matrix; matmul has self.ncols != other.nrows; solve has self.nrows != b.len(); augment has mismatched row counts.

RightHandSideIsNotVector

Meaning: The right-hand side is not a column vector.

Triggered when: Not used directly by solve in the current version (solve accepts a &[D::Element] slice), but kept for extension.

Inconsistent

Meaning: The linear system has no solution (inconsistent).

Triggered when: In solve, after elimination a free row of the augmented matrix has a nonzero entry in the last (constant) column.

Underdetermined { rank }

Meaning: The linear system has infinitely many solutions (underdetermined).

Triggered when: In solve, the rank rank of the coefficient matrix is less than the number of variables nvars.

ResultNotInDomain

Meaning: The solution does not lie in the target domain.

Triggered when: In the final division step of solve, div returns None (e.g., not divisible in an integer domain).

Display output:

VariantOutput
ShapeMismatch"matrix shape mismatch"
RightHandSideIsNotVector"right-hand side must be a column vector"
Inconsistent"inconsistent linear system"
Underdetermined { rank }"underdetermined system (rank N)"
ResultNotInDomain"solution does not lie in the expected domain"

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::{Matrix, MatrixError};

let d = IntegerDomain;

// Inconsistent system: identical rows but different constant terms
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(1)],
         vec![Integer::from(1), Integer::from(1)]],
    d,
);
let b = vec![Integer::from(1), Integer::from(2)];
assert_eq!(a.solve(&b), Err(MatrixError::Inconsistent));
}

See also: solve


Matrix<D>

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Matrix<D: EuclideanDomain> {
    data: Vec<D::Element>,   // row-major storage
    nrows: usize,
    ncols: usize,
    domain: D,
}
}

Function: A dense matrix over the domain D, with elements stored row-major in a Vec<D::Element>.

Design invariants:

  • data.len() == nrows * ncols (guaranteed by constructors)
  • Element (i, j) is at position i * ncols + j in data
  • D is constrained to EuclideanDomain, supporting div, gcd, etc., which makes fraction-free elimination possible

Trait implementations:

  • Index<(usize, usize)> / IndexMut<(usize, usize)> — element access via matrix[(i, j)]
  • Display — one line per row, elements separated by spaces
  • Debug, Clone, PartialEq, Eq

Constructors

new

Signature: pub fn new(nrows: usize, ncols: usize, data: Vec<D::Element>, domain: D) -> Self

Function: Creates a matrix from row-major one-dimensional data.

Parameters:

ParameterTypeDescription
nrowsusizeNumber of rows
ncolsusizeNumber of columns
dataVec<D::Element>Elements in row-major order; length must equal nrows * ncols
domainDThe coefficient domain

Return value: Matrix<D>

Errors: Panics at runtime if data.len() != nrows * ncols.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
// Create a 2×3 matrix:
// [ 1  2  3 ]
// [ 4  5  6 ]
let m = Matrix::new(2, 3, vec![
    Integer::from(1), Integer::from(2), Integer::from(3),
    Integer::from(4), Integer::from(5), Integer::from(6),
], d);
assert_eq!(m.nrows(), 2);
assert_eq!(m.ncols(), 3);
assert_eq!(m[(0, 0)], Integer::from(1));
assert_eq!(m[(1, 2)], Integer::from(6));
}

See also: from_rows, zeros, identity


zeros

Signature: pub fn zeros(nrows: usize, ncols: usize, domain: D) -> Self

Function: Creates a zero matrix of the given shape.

Parameters:

ParameterTypeDescription
nrowsusizeNumber of rows
ncolsusizeNumber of columns
domainDThe coefficient domain

Return value: A Matrix<D> with all elements equal to domain.zero().

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let z = Matrix::zeros(2, 3, d);
assert_eq!(z[(0, 0)], Integer::from(0));
assert_eq!(z[(1, 2)], Integer::from(0));
}

See also: identity, new


identity

Signature: pub fn identity(n: usize, domain: D) -> Self

Function: Creates the n × n identity matrix.

Parameters:

ParameterTypeDescription
nusizeMatrix dimension
domainDThe coefficient domain

Return value: A square matrix with domain.one() on the diagonal and domain.zero() elsewhere.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let id = Matrix::identity(3, d);
assert_eq!(id[(0, 0)], Integer::from(1));
assert_eq!(id[(0, 1)], Integer::from(0));
assert_eq!(id[(2, 2)], Integer::from(1));
}

See also: zeros


from_rows

Signature: pub fn from_rows(rows: Vec<Vec<D::Element>>, domain: D) -> Self

Function: Creates a matrix from nested row vectors.

Parameters:

ParameterTypeDescription
rowsVec<Vec<D::Element>>Each inner Vec is one row; all rows must have the same length
domainDThe coefficient domain

Return value: Matrix<D>

Errors: Panics at runtime if rows have inconsistent lengths.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::from_rows(
    vec![
        vec![Integer::from(1), Integer::from(2), Integer::from(3)],
        vec![Integer::from(4), Integer::from(5), Integer::from(6)],
    ],
    d,
);
assert_eq!(m.nrows(), 2);
assert_eq!(m.ncols(), 3);
assert_eq!(m[(0, 2)], Integer::from(3));
}

See also: new, into_rows


Accessor Methods

nrows

Signature: pub fn nrows(&self) -> usize

Function: Returns the number of rows of the matrix.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::new(3, 2, vec![Integer::from(0); 6], d);
assert_eq!(m.nrows(), 3);
}

ncols

Signature: pub fn ncols(&self) -> usize

Function: Returns the number of columns of the matrix.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::new(3, 2, vec![Integer::from(0); 6], d);
assert_eq!(m.ncols(), 2);
}

shape

There is no separate shape() method. The shape is obtained by combining nrows() and ncols():

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::new(3, 4, vec![Integer::from(0); 12], d);
let (rows, cols) = (m.nrows(), m.ncols());
assert_eq!((rows, cols), (3, 4));
}

domain

Signature: pub fn domain(&self) -> &D

Function: Returns a reference to the matrix's coefficient domain.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, EuclideanDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::zeros(1, 1, d);
let dom = m.domain();
assert!(dom.is_zero(&dom.zero()));
}

data

Signature: pub fn data(&self) -> &[D::Element]

Function: Returns a read-only slice of the underlying row-major storage.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
let raw = m.data();
assert_eq!(raw.len(), 4);
assert_eq!(raw[0], Integer::from(1));  // (0,0)
assert_eq!(raw[3], Integer::from(4));  // (1,1)
}

row

Signature: pub fn row(&self, i: usize) -> Vec<D::Element>

Function: Returns a copy of the i-th row.

Parameters:

ParameterTypeDescription
iusizeRow index, 0-based

Return value: Vec<D::Element> of length ncols.

Errors: Panics if the index is out of bounds.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
let r = m.row(1);
assert_eq!(r, vec![Integer::from(3), Integer::from(4)]);
}

See also: column, into_rows


column

Signature: pub fn column(&self, j: usize) -> Vec<D::Element>

Function: Returns a copy of the j-th column.

Parameters:

ParameterTypeDescription
jusizeColumn index, 0-based

Return value: Vec<D::Element> of length nrows.

Errors: Panics if the index is out of bounds.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
let c = m.column(0);
assert_eq!(c, vec![Integer::from(1), Integer::from(3)]);
}

See also: row


into_rows

Signature: pub fn into_rows(self) -> Vec<Vec<D::Element>>

Function: Consumes the matrix and converts it into nested row vectors.

Return value: Outer Vec of length nrows, each inner Vec of length ncols.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
let rows = m.into_rows();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], vec![Integer::from(1), Integer::from(2)]);
assert_eq!(rows[1], vec![Integer::from(3), Integer::from(4)]);
}

See also: from_rows, row


Indexing & Display

Index<(usize, usize)>

Signature: impl Index<(usize, usize)> for Matrix<D>

Function: Read-only access to element (i, j) via matrix[(i, j)], equivalent to data[i * ncols + j].

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::from_rows(
    vec![vec![Integer::from(5), Integer::from(6)]], d,
);
assert_eq!(m[(0, 0)], Integer::from(5));
assert_eq!(m[(0, 1)], Integer::from(6));
}

IndexMut<(usize, usize)>

Signature: impl IndexMut<(usize, usize)> for Matrix<D>

Function: Writable access to elements via matrix[(i, j)] = val.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let mut m = Matrix::zeros(2, 2, d);
m[(0, 1)] = Integer::from(42);
m[(1, 0)] = Integer::from(-7);
assert_eq!(m[(0, 1)], Integer::from(42));
assert_eq!(m[(1, 0)], Integer::from(-7));
}

Display

Signature: impl Display for Matrix<D> where D::Element: Display

Function: Formats the matrix with one row per line, elements separated by spaces.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let m = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
let s = format!("{:?}", m);
// Output: Debug representation (IntegerDomain does not implement Display, and Matrix's Display requires it)
assert!(s.contains("1"));
}

Matrix Operations

swap_rows

Signature: pub fn swap_rows(&mut self, i: usize, j: usize, start_col: usize)

Function: Swaps row i and row j, starting from column start_col.

Parameters:

ParameterTypeDescription
iusizeIndex of the first row
jusizeIndex of the second row
start_colusizeStart column (inclusive); elements before it are unchanged

Return value: None (in-place modification).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let mut m = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
m.swap_rows(0, 1, 0);
assert_eq!(m[(0, 0)], Integer::from(3));
assert_eq!(m[(0, 1)], Integer::from(4));
assert_eq!(m[(1, 0)], Integer::from(1));
assert_eq!(m[(1, 1)], Integer::from(2));
}

transpose

Signature: pub fn transpose(&self) -> Matrix<D>

Function: Returns the transpose of the matrix. $(A^T){ij} = A{ji}$.

Return value: A new Matrix<D> of shape (ncols, nrows).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2), Integer::from(3)],
         vec![Integer::from(4), Integer::from(5), Integer::from(6)]],
    d,
);
let t = a.transpose();
assert_eq!(t.nrows(), 3);
assert_eq!(t.ncols(), 2);
assert_eq!(t[(0, 0)], Integer::from(1));
assert_eq!(t[(0, 1)], Integer::from(4));
assert_eq!(t[(2, 1)], Integer::from(6));
}

See also: trace


trace

Signature: pub fn trace(&self) -> Result<D::Element, MatrixError>

Function: Computes the trace of a square matrix, i.e. the sum of the diagonal elements: $\text{tr}(A) = \sum_{i=0}^{n-1} a_{ii}$.

Return value:

  • Ok(sum) — the trace
  • Err(MatrixError::ShapeMismatch) — the matrix is not square

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
// tr = 1 + 4 = 5
assert_eq!(a.trace().unwrap(), Integer::from(5));
}

Non-square error:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::{Matrix, MatrixError};

let d = IntegerDomain;
let m = Matrix::from_rows(vec![vec![Integer::from(1), Integer::from(2)]], d);
assert_eq!(m.trace(), Err(MatrixError::ShapeMismatch));
}

See also: determinant


matmul

Signature: pub fn matmul(&self, other: &Matrix<D>) -> Result<Matrix<D>, MatrixError>

Function: Computes the matrix product $C = AB$, where $C_{ij} = \sum_k A_{ik} \cdot B_{kj}$.

Parameters:

ParameterTypeDescription
other&Matrix<D>The matrix to multiply on the right

Return value:

  • Ok(Matrix) — the product matrix of shape (self.nrows, other.ncols)
  • Err(MatrixError::ShapeMismatch) — self.ncols != other.nrows

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
let b = Matrix::from_rows(
    vec![vec![Integer::from(5), Integer::from(6)],
         vec![Integer::from(7), Integer::from(8)]],
    d,
);
let c = a.matmul(&b).unwrap();
// C = [[1*5+2*7, 1*6+2*8], [3*5+4*7, 3*6+4*8]]
//   = [[19, 22], [43, 50]]
assert_eq!(c[(0, 0)], Integer::from(19));
assert_eq!(c[(0, 1)], Integer::from(22));
assert_eq!(c[(1, 0)], Integer::from(43));
assert_eq!(c[(1, 1)], Integer::from(50));
}

Shape mismatch:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::{Matrix, MatrixError};

let d = IntegerDomain;
let a = Matrix::from_rows(vec![vec![Integer::from(1), Integer::from(2)]], d);
let b = Matrix::from_rows(vec![vec![Integer::from(3), Integer::from(4)]], d);
// a: 1×2, b: 1×2 → ncols(a)=2 ≠ nrows(b)=1
assert_eq!(a.matmul(&b), Err(MatrixError::ShapeMismatch));
}

See also: solve


determinant

Signature: pub fn determinant(&self) -> Result<D::Element, MatrixError>

Function: Computes the determinant of a square matrix using the Bareiss fraction-free algorithm (with partial pivoting).

Algorithm highlights:

  • Reduces the matrix to upper triangular form by elimination, dividing at each step by the previous pivot (Bareiss guarantees exact divisibility)
  • Partial pivoting: when a zero pivot is encountered, searches downward for a nonzero row and swaps
  • If an entire column becomes zero during elimination, the determinant is zero
  • The sign is determined by the parity of the number of row swaps

Return value:

  • Ok(det) — the determinant
  • Err(MatrixError::ShapeMismatch) — the matrix is not square

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;

// 2×2: det = 1*4 - 2*3 = -2
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(3), Integer::from(4)]],
    d,
);
assert_eq!(a.determinant().unwrap(), Integer::from(-2));
}

Singular matrix:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
// The second row is 2× the first row
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(2), Integer::from(4)]],
    d,
);
assert_eq!(a.determinant().unwrap(), Integer::from(0));
}

See also: inverse, rank


rank

Signature: pub fn rank(&self) -> usize

Function: Computes the rank of the matrix via fraction-free Gaussian elimination. Clones the matrix, runs row_echelon, and returns the number of nonzero rows.

Return value: usize — the rank of the matrix.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;

// Full-rank 2×2
let id = Matrix::identity(2, d);
assert_eq!(id.rank(), 2);

// Rank-deficient: second row = 2 × first row
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2), Integer::from(3)],
         vec![Integer::from(2), Integer::from(4), Integer::from(6)],
         vec![Integer::from(1), Integer::from(1), Integer::from(1)]],
    d,
);
assert_eq!(a.rank(), 2);
}

See also: determinant, row_echelon


inverse

Signature: pub fn inverse(&self) -> Result<Matrix<D>, MatrixError>

Function: Computes the inverse matrix $A^{-1}$ of a square matrix, by solving $A x_j = e_j$ column by column ($e_j$ is the $j$-th column of the identity matrix).

Return value:

  • Ok(Matrix) — the inverse, satisfying $A \cdot A^{-1} = I$
  • Err(MatrixError::ShapeMismatch) — the matrix is not square
  • Err(MatrixError::Inconsistent) — the matrix is singular (no solution)
  • Err(MatrixError::Underdetermined { .. }) — singularity leads to an underdetermined system
  • Err(MatrixError::ResultNotInDomain) — an entry of the inverse is not in the domain

⚠️ Note: Over the integer domain IntegerDomain, only unimodular matrices ($\det = \pm 1$) have integer inverses. Invert general matrices over the rational domain RationalDomain.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
// Unimodular matrix: det = 1*1 - 0*2 = 1
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(0), Integer::from(1)]],
    d,
);
let inv = a.inverse().unwrap();
assert_eq!(inv[(0, 0)], Integer::from(1));
assert_eq!(inv[(0, 1)], Integer::from(-2));
assert_eq!(inv[(1, 0)], Integer::from(0));
assert_eq!(inv[(1, 1)], Integer::from(1));

// Verify A * A^{-1} = I
let prod = a.matmul(&inv).unwrap();
assert_eq!(prod, Matrix::identity(2, IntegerDomain));
}

Singular matrix error:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(2)],
         vec![Integer::from(2), Integer::from(4)]],
    d,
);
assert!(a.inverse().is_err());
}

See also: solve, determinant


augment

Signature: pub fn augment(&self, other: &Matrix<D>) -> Result<Matrix<D>, MatrixError>

Function: Horizontally concatenates two matrices, producing the augmented matrix $[A \mid B]$.

Parameters:

ParameterTypeDescription
other&Matrix<D>The matrix to concatenate on the right

Return value:

  • Ok(Matrix) — the augmented matrix of shape (nrows, ncols_a + ncols_b)
  • Err(MatrixError::ShapeMismatch) — the two matrices have different row counts

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let a = Matrix::from_rows(
    vec![vec![Integer::from(3), Integer::from(2)],
         vec![Integer::from(1), Integer::from(1)]],
    d,
);
let b = Matrix::new(2, 1, vec![Integer::from(5), Integer::from(2)], d);
let aug = a.augment(&b).unwrap();
assert_eq!(aug.nrows(), 2);
assert_eq!(aug.ncols(), 3);
assert_eq!(aug[(0, 0)], Integer::from(3));
assert_eq!(aug[(0, 2)], Integer::from(5));
}

See also: solve


solve

Signature: pub fn solve(&self, b: &[D::Element]) -> Result<Vec<D::Element>, MatrixError>

Function: Solves the linear system $Ax = b$, using fraction-free Gaussian elimination with back substitution.

Parameters:

ParameterTypeDescription
b&[D::Element]The right-hand side vector; length must equal self.nrows

Return value:

  • Ok(Vec<D::Element>) — the solution vector of length self.ncols
  • Err(MatrixError::ShapeMismatch) — b.len() != self.nrows
  • Err(MatrixError::Inconsistent) — the system has no solution
  • Err(MatrixError::Underdetermined { rank }) — infinitely many solutions, rank < ncols
  • Err(MatrixError::ResultNotInDomain) — the solution is not in the domain (e.g., integer division fails)

Algorithm steps:

  1. Build the augmented matrix $[A \mid b]$
  2. Run fraction-free row echelon reduction (row_echelon) on the first ncols columns
  3. Check whether the constant terms of the rows below the rank are zero (consistency check)
  4. Run fraction-free back substitution (back_substitution)
  5. If the rank < number of variables, return Underdetermined
  6. Divide by the pivots to obtain the solution

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;

// 2x + y = 4
// x  + y = 3  → x = 1, y = 2
let a = Matrix::from_rows(
    vec![vec![Integer::from(2), Integer::from(1)],
         vec![Integer::from(1), Integer::from(1)]],
    d,
);
let b = vec![Integer::from(4), Integer::from(3)];
let x = a.solve(&b).unwrap();
assert_eq!(x, vec![Integer::from(1), Integer::from(2)]);
}

3×3 system:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
// x + y + z = 6
// 2x - y + z = 3
// x + 2y - z = 2  → x=1, y=2, z=3
let a = Matrix::from_rows(
    vec![
        vec![Integer::from(1), Integer::from(1), Integer::from(1)],
        vec![Integer::from(2), Integer::from(-1), Integer::from(1)],
        vec![Integer::from(1), Integer::from(2), Integer::from(-1)],
    ],
    d,
);
let b = vec![Integer::from(6), Integer::from(3), Integer::from(2)];
let x = a.solve(&b).unwrap();
assert_eq!(x, vec![Integer::from(1), Integer::from(2), Integer::from(3)]);
}

Inconsistent system:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::{Matrix, MatrixError};

let d = IntegerDomain;
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(1)],
         vec![Integer::from(1), Integer::from(1)]],
    d,
);
let b = vec![Integer::from(1), Integer::from(2)];
assert_eq!(a.solve(&b), Err(MatrixError::Inconsistent));
}

Underdetermined system:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::{Matrix, MatrixError};

let d = IntegerDomain;
let a = Matrix::from_rows(
    vec![vec![Integer::from(1), Integer::from(1), Integer::from(1)]],
    d,
);
let b = vec![Integer::from(3)];
assert!(matches!(a.solve(&b), Err(MatrixError::Underdetermined { .. })));
}

See also: inverse, augment


Internal Elimination Methods

The following two methods are pub; they usually do not need to be called directly, but may be useful in advanced scenarios (e.g., custom elimination pipelines).

row_echelon

Signature: pub fn row_echelon(&mut self, max_col: usize) -> usize

Function: Runs fraction-free Gaussian elimination on the first max_col columns, reducing the matrix to row echelon form. Modifies the matrix in place. Returns the rank (number of nonzero rows).

Parameters:

ParameterTypeDescription
max_colusizeThe maximum number of columns involved in the elimination (exclusive); automatically clamped to ncols

Return value: usize — the rank of the matrix.

Algorithm details:

  • Finds a nonzero pivot in each column, swapping rows if necessary
  • Extracts the GCD content from the pivot row to prevent coefficient growth
  • Elimination uses gcd-reduced fraction-free scaling: with $g = \gcd(\text{pivot}, a_{kj})$, the eliminated row is scaled by $\text{pivot} / g$ and the pivot row by $a_{kj} / g$ before subtracting, so no fractions appear

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let mut a = Matrix::from_rows(
    vec![
        vec![Integer::from(1), Integer::from(2), Integer::from(3)],
        vec![Integer::from(2), Integer::from(4), Integer::from(6)],
        vec![Integer::from(0), Integer::from(1), Integer::from(1)],
    ],
    d,
);
let rank = a.row_echelon(3);
assert_eq!(rank, 2);
}

See also: back_substitution, rank


back_substitution

Signature: pub fn back_substitution(&mut self, max_col: usize)

Function: Runs fraction-free back substitution on a matrix already in row echelon form. Modifies the first max_col columns of the matrix in place.

Algorithm details:

  • Processes rows from the bottom up
  • For each pivot row, first extracts the GCD content
  • For rows above the pivot, eliminates the pivot column with the same gcd-reduced scaling (the row above is scaled by $\text{pivot} / g$ and the pivot row by $a_{kj} / g$, where $g = \gcd(\text{pivot}, a_{kj})$) before subtracting

See also: row_echelon, solve


Complete Example

The following comprehensive example demonstrates the basic matrix workflow: construction, operations, and solving.

use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

fn main() {
    let d = IntegerDomain;

    // Construct matrix A
    let a = Matrix::from_rows(
        vec![
            vec![Integer::from(2), Integer::from(1), Integer::from(-1)],
            vec![Integer::from(-3), Integer::from(-1), Integer::from(2)],
            vec![Integer::from(-2), Integer::from(1), Integer::from(2)],
        ],
        d,
    );

    // Determinant
    let det = a.determinant().unwrap();
    assert_eq!(det, Integer::from(-1)); // det = -1 (unimodular)

    // Rank
    assert_eq!(a.rank(), 3);

    // Trace
    let tr = a.trace().unwrap();
    assert_eq!(tr, Integer::from(3)); // 2 + (-1) + 2

    // Transpose
    let at = a.transpose();
    assert_eq!(at[(0, 1)], Integer::from(-3));

    // Solve Ax = b
    let b = vec![Integer::from(8), Integer::from(-11), Integer::from(-3)];
    let x = a.solve(&b).unwrap();
    assert_eq!(x, vec![Integer::from(2), Integer::from(3), Integer::from(-1)]);

    // Inverse (det = -1, so an integer inverse exists)
    let inv = a.inverse().unwrap();
    // Verify A * A^{-1} = I
    let prod = a.matmul(&inv).unwrap();
    assert_eq!(prod, Matrix::identity(3, IntegerDomain));

    // Matrix multiplication
    let c = a.matmul(&a).unwrap();
    assert_eq!(c.nrows(), 3);
    assert_eq!(c.ncols(), 3);
}

See also

Calculus

Source: ocas-calc/src/

oCAS's calculus module provides symbolic differentiation, integration, series expansion, and partial fraction decomposition. All operations execute on an AtomArena and produce new Atom handles. Derivatives or integrals that cannot be simplified are returned in unevaluated form as Derivative(expr, var) or Integral(expr, var).

Module structure:

SubmodulePurpose
derivativesymbolic differentiation diff
integrallayered integration pipeline integrate, integrate_heuristic, integrate_with_fuel
seriesTaylor expansion taylor, substitution substitute
partial_fractionpartial fraction decomposition apart, recomposition together
towerdifferential field tower construction for the Risch algorithm
odeordinary differential equation solving (see Solvers)

diff

Signature:

#![allow(unused)]
fn main() {
pub fn diff<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a>
}

Description: computes the symbolic derivative of expr with respect to var.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>the expression arena used to construct the result
exprAtom<'a>the expression to differentiate
varSymbolthe differentiation variable (interned string)

Returns: Atom<'a> — the differentiated and simplified expression.

Algorithm:

diff recursively traverses the expression tree and applies the standard differentiation rules to each node type:

Node typeDifferentiation rule
Num(_)$\frac{d}{dx} c = 0$
Var(s)$1$ if $s = \text{var}$, otherwise $0$
Add([a₁, …, aₙ])derivative of a sum = sum of derivatives: $\sum \frac{d a_i}{dx}$
Mul([a₁, …, aₙ])product rule: $\frac{d}{dx}\prod a_i = \sum_i \left(\frac{d a_i}{dx} \prod_{j \neq i} a_j\right)$
Pow([base, exp])power rule (with chain rule), supports constant and non-constant exponents
Fun(name, args)built-in function table + chain rule

Built-in derivative table:

Function $f(u)$Derivative $f'(u) \cdot u'$
$\sin(u)$$\cos(u) \cdot u'$
$\cos(u)$$-\sin(u) \cdot u'$
$\tan(u)$$\sec^2(u) \cdot u'$
$\sec(u)$$\sec(u)\tan(u) \cdot u'$
$\exp(u)$$\exp(u) \cdot u'$
$\log(u)$$u^{-1} \cdot u'$
$\sqrt{u}$$(2\sqrt{u})^{-1} \cdot u'$
$\text{atan}(u)$$(1 + u^2)^{-1} \cdot u'$

The table is hard-coded in diff_function in derivative.rs. Functions not in the table (such as $\text{asin}$, $\sinh$, $\cosh$, $\tanh$, etc.) also return the unevaluated form Derivative(f, var).

Unevaluated form: when the function being differentiated is not in the built-in table, the result is Derivative(expr, var), which can be detected by later pattern matching (match a Fun node whose name is "Derivative"; note that is_fallback only detects "Integral", not Derivative).

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::diff;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");

// d/dx [sin(x)] = cos(x)
let sin_x = ctx.fun("sin", &[x]);
let result = diff(&ctx, sin_x, Symbol::new("x"));
assert_eq!(result.to_string(), "cos(x)");

// d/dx [x^3] = 3*x^2
let x_cubed = ctx.pow(x, ctx.num(3));
let result = diff(&ctx, x_cubed, Symbol::new("x"));
assert_eq!(result.to_string(), "3*(x^2)");

// d/dx [exp(x^2)] = 2*x*exp(x^2) (chain rule)
let inner = ctx.pow(x, ctx.num(2));
let expr = ctx.fun("exp", &[inner]);
let result = diff(&ctx, expr, Symbol::new("x"));
// Output contains a simplified form of 2*x*exp(x^2)

// Unknown functions return the Derivative form
let f = ctx.fun("my_func", &[x]);
let result = diff(&ctx, f, Symbol::new("x"));
// result contains Derivative(my_func(x), x)
}

See also: taylor, integrate


integrate

Signature:

#![allow(unused)]
fn main() {
pub fn integrate<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a>
}

Description: computes the symbolic integral of expr with respect to var. Uses a layered integration pipeline that tries different algorithms layer by layer.

0.28.0: the returned result is not certified (it may be a pipeline candidate). Use integrate_outcome when you need an antiderivative with a machine-checkable symbolic certificate.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>the expression arena
exprAtom<'a>the integrand
varSymbolthe integration variable

Returns: Atom<'a> — the integration result. Returns Integral(expr, var) when unsolved.

Integration pipeline:

integrate internally calls integrate_raw (recursive, depth limit MAX_DEPTH = 8). The pipeline has two phases:

┌───────────────────────────────────────────┐
│ Phase 1: integrate_raw dispatches on node │
│   type                                    │
├───────────────────────────────────────────┤
│ Num / Var          → constant integration:│
│                      c·x; ∫x dx = (1/2)·x² │
│ Add                → integrate term-wise  │
│ Mul (integrate_product)  → extract the    │
│                      constant factor      │
│ Pow (integrate_power)    → power rule     │
│                      ∫xⁿ, ∫(a·x+b)ⁿ,      │
│                      fractional (a·x+b)^(p/q)│
│ Fun (integrate_function) → table + linear │
│                      substitution         │
│                      ∫sin(u), ∫cos(u),    │
│                      ∫exp(u), ∫log(u)     │
│                      if u = a·x+b, apply  │
│                      the chain rule       │
├───────────────────────────────────────────┤
│ Phase 2: when the dispatch above fails    │
│   (returns the fallback form)             │
│   → try_risch_or_fallback, in order:      │
├───────────────────────────────────────────┤
│  1. rational function      (integrate_rational)      │
│  2. quadratic/linear denom power recurrences         │
│     (integrate_quad_power)                           │
│  3. bounded-expansion pre-pass (expand_prepass, 0.27.2)│
│  3b. exact linear-square fold (fold_linear_squares,   │
│      p²+2pq+q² → (p+q)², affine base, 0.27.3)         │
│  4. rational-derivative kernel substitution          │
│     (integrate_kernel_subst, tan/cot/tanh/coth, 0.27.2)│
│  5. hyperbolic closed-form family                    │
│     (integrate_hyperbolic_reduction, 0.27.2)         │
│  6. symbolic-constant rationals                      │
│     (integrate_rational_symbolic, ℚ(symbols))        │
│  7. Risch algorithm        (risch_integrate)         │
│  8. trig→exponential + Risch + realify               │
│  9. special functions      (special_integrate)       │
│ 10. rule-table engine      (integrate_rules, A–H)    │
│ 11. sqrt-quadratic engine  (integrate_sqrt_quadratic)│
│ 12. Chebyshev binomials    (integrate_binomial)      │
│ 13. exp/log kernels        (integrate_exp_log)       │
│ 14. inverse-trig/hyperbolic (integrate_inverse_trig) │
│ 15. trig product-to-sum    (trig_reduce_products)    │
│ 16. trig-denominator power reductions                │
│     (integrate_trig_reduction)                       │
│ 17. single-trig-kernel rationals + tan/sec           │
│     (integrate_trig_kernel)                          │
│ 18. bounded-expansion retry (expand_bounded)         │
│ 19. half-power front-end   (integrate_half_power, 0.27.2)│
│ 20. elliptic reduction     (integrate_elliptic,      │
│     EllipticF/E/Pi, 0.27.2)                          │
│ 21. heuristics             (heuristic_integrate)     │
│     parts (LIATE), trig substitution,                │
│     Weierstrass, Euler substitution                  │
│ 22. unevaluated form       (fallback)                │
│     Integral(expr, var)                              │
└───────────────────────────────────────────┘

OCAS_INTEGRATE_TRACE=1 prints [trace] enter <stage> :: <expr> / [trace] decline <stage> for every stage, which attributes a hanging or falling-back case to a stage without guesswork. OCAS_INTEGRATE_RULES=0 disables only stage 10 (the rule table).

Stages 3 and 3b are passes, not traced stages: they rewrite the integrand at the pipeline entry before any mechanism sees it. The linear-square fold exists because the corpus writes a linear form squared as an expanded trinomial (a² + 2abx + b²x²), which the symbolic-rational backend grinds on — rubi-00854 was a 10 s per-case timeout before the fold and solves in 0.01 s after it. The fold is gated two ways so it cannot misfire: the reconstructed base must be affine in the integration variable (so it never re-forms the trigonometric squares the product-to-sum stage needs expanded), and the rewrite is accepted only when re-expanding the candidate reproduces the input sum (exactly, or after normalize). It is never applied under a non-integer power, where ((p+q)²)^{1/2} = |p+q| ≠ p+q.

Phase 1 — node-type dispatch:

  • Num(_): the integral of a constant is the linear function c * x
  • Var(var): $\int x,dx = \frac{1}{2} x^2$; other variables are treated as constants
  • Add: integrate each term recursively and sum
  • Mul (integrate_product): recognizes the form c · f(x) (a constant times a non-constant factor), extracts the constant factor, and integrates recursively
  • Pow (integrate_power): power-function integration, supporting $\int x^n,dx$, linear forms $(a \cdot x + b)^n$ (including $n = -1$: $\int \frac{dx}{ax+b} = \frac{\log(ax+b)}{a}$), and fractional exponents $(a \cdot x + b)^{p/q}$
  • Fun (integrate_function): table lookup + linear substitution

Table lookup and linear substitution:

The built-in integral table covers power functions (integrate_power) plus sin, cos, exp, log. When the argument of the integrand (or the base of a power) has the linear form $a \cdot x + b$, the chain rule $\int f(ax+b),dx = \frac{1}{a} F(ax+b)$ is applied automatically.

Phase 2 — fallback chain (try_risch_or_fallback):

  1. Rational function integration (integrate_rational): when the expression is a rational function of $\var$ (with no other variables or function applications), perform:

    1. Integrate the polynomial part term by term
    2. Hermite reduction to separate the rational part
    3. Logarithmic part: completing the square (quadratic denominators → $\log$ or $\text{atan}$) + Rothstein–Trager resultant

    See rational function integration.

  2. Risch algorithm (risch_integrate): constructs the differential field tower $\mathbb{Q}(x, t_1, \dots, t_n)$ and integrates recursively layer by layer. See Risch algorithm.

  3. Trigonometric rewriting: when Risch fails on the original expression and the integrand contains trigonometric functions, they are rewritten into complex exponential form via trig_to_exp, Risch is tried again, and realify converts the answer back to real form.

  4. Special functions (special_integrate): when both Risch and the trigonometric rewriting fail, tries to match known special-function antiderivatives. See special function integration.

  5. Heuristics (heuristic_integrate): integration by parts (LIATE), trigonometric substitution, Weierstrass substitution, Euler substitution. See integrate_heuristic.

  6. Unevaluated form (fallback): when everything above fails, returns Integral(expr, var). Detectable via is_fallback:

#![allow(unused)]
fn main() {
pub(crate) fn is_fallback<'a>(atom: &Atom<'a>) -> bool {
    matches!(atom.node(), AtomNode::Fun(name, _) if name.as_str() == "Integral")
}
}

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::integrate;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");

// ∫ x² dx = (1/3) x³
let expr = ctx.pow(x, ctx.num(2));
let result = integrate(&ctx, expr, Symbol::new("x"));
assert_eq!(result.to_string(), "(3^-1)*(x^3)");

// ∫ sin(x) dx = -cos(x)
let expr = ctx.fun("sin", &[x]);
let result = integrate(&ctx, expr, Symbol::new("x"));
// Output: a simplified form of -cos(x)

// ∫ 1/x dx = log(x) (via the rational function integrator or Risch)
let expr = ctx.pow(x, ctx.num(-1));
let result = integrate(&ctx, expr, Symbol::new("x"));
// Output: log(x)

// Non-integrable expressions return the Integral form
let expr = ctx.fun("my_func", &[x]);
let result = integrate(&ctx, expr, Symbol::new("x"));
// result is Integral(my_func(x), x)
}

See also: integrate_heuristic, integrate_with_fuel, rational function integration, Risch algorithm


integrate_outcome

Signature (new in 0.28.0):

#![allow(unused)]
fn main() {
pub fn integrate_outcome<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
) -> Outcome<'a>
pub fn integrate_outcome_with_options<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    options: IntegrateOptions,
) -> Outcome<'a>
}

Description: certified integration. Returns the three-valued [Outcome]:

ValueMeaning
Found { value, certificate }An antiderivative with a machine-checkable symbolic certificate (D(F) − f ≡ 0 in the exact checker)
ProvedNonElementary { witness }Proved non-elementary; no producer in 0.28.0 (the variant is reserved for the non-elementary layer)
Unknown { residue, uncertified }Honest "I do not know": residue is Integral(f, var); uncertified is the pipeline's candidate, not an answer, diagnostics only

A result the exact checker falsifies is never returned as Found (and every Risch result is additionally verified inside the tower's own field).

Example:

#![allow(unused)]
fn main() {
use ocas::prelude::*;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let two_x = ctx.mul(&[ctx.num(2), x]);
let outcome = integrate_outcome(&ctx, two_x, Symbol::new("x"));
assert!(outcome.is_found());
println!("{}", outcome.value());          // (2^-1)*(x^2)
println!("{:?}", outcome.certificate());  // Some(Certificate { .. })
}

See also: integrate, [Certificate], [CertDecline]


integrate_heuristic

Signature:

#![allow(unused)]
fn main() {
pub fn integrate_heuristic<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a>
}

Description: attempts heuristic integration techniques. Uses only integration by parts, trigonometric substitution, Weierstrass substitution, and Euler substitution; does not invoke the Risch algorithm or the rational function integrator.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>the expression arena
exprAtom<'a>the integrand
varSymbolthe integration variable

Returns: Atom<'a> — the integration result if a heuristic succeeds, otherwise Integral(expr, var).

Heuristic methods:

1. Integration by parts (LIATE heuristic)

For products $\int u \cdot v' , dx$, choose $u$ by LIATE priority (highest to lowest):

PriorityTypeLIATE scoreExample
0 (highest)Logarithmic (L)0$\log(x)$
1Inverse trigonometric (I)1$\text{asin}(x)$
2Algebraic (A)2$x^2$
3Trigonometric (T)3$\sin(x)$
4Exponential (E)4$\exp(x)$
5 (lowest)Other5—

Choose the factor with the lowest score as $u$ and the rest as $v'$. The recursion depth limit is PARTS_MAX_DEPTH = 2.

2. Trigonometric substitution

Matches the following patterns and returns the known antiderivative:

IntegrandSubstitutionResult
$\frac{1}{\sqrt{a^2 - x^2}}$$x = a\sin\theta$$\text{asin}(x/a)$
$\frac{1}{\sqrt{a^2 + x^2}}$$x = a\sinh t$$\text{asinh}(x/a)$
$\frac{1}{\sqrt{x^2 - a^2}}$$x = a\cosh t$$\text{acosh}(x/a)$
$\sqrt{a^2 - x^2}$$x = a\sin\theta$$\frac{x\sqrt{a^2-x^2} + a^2,\text{asin}(x/a)}{2}$
$\sqrt{a^2 + x^2}$$x = a\sinh t$$\frac{x\sqrt{a^2+x^2} + a^2,\text{asinh}(x/a)}{2}$
$\sqrt{x^2 - a^2}$$x = a\cosh t$$\frac{x\sqrt{x^2-a^2} - a^2,\text{acosh}(x/a)}{2}$

3. Weierstrass substitution

When the integrand is a rational function of $\sin(u)$ and $\cos(u)$ ($u$ linear in $\text{var}$), apply the substitution $t = \tan(u/2)$:

$$\sin(u) = \frac{2t}{1+t^2}, \quad \cos(u) = \frac{1-t^2}{1+t^2}$$

Rationalize the trigonometric rational function into a rational function of $t$, then integrate recursively.

4. Euler substitution

When the integrand contains $\sqrt{ax^2 + bx + c}$, try the Euler substitution to eliminate the radical.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::integrate_heuristic;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");

// ∫ x·exp(x) dx — integration by parts (LIATE: x=algebraic, exp=exponential)
let expr = ctx.mul(&[x, ctx.fun("exp", &[x])]);
let result = integrate_heuristic(&ctx, expr, Symbol::new("x"));
// Result contains a simplified form of (x-1)*exp(x)

// ∫ 1/√(1 - x²) dx = asin(x)
// Build (1 - x²)^(-1/2): the exponent is -1·(2^-1)
let one = ctx.num(1);
let sqrt_arg = ctx.add(&[one, ctx.mul(&[ctx.num(-1), ctx.pow(x, ctx.num(2))])]);
let neg_half = ctx.mul(&[ctx.num(-1), ctx.pow(ctx.num(2), ctx.num(-1))]);
let expr = ctx.pow(sqrt_arg, neg_half);
let result = integrate_heuristic(&ctx, expr, Symbol::new("x"));
// Output: a simplified form of asin(x)
}

See also: integrate


integrate_with_fuel

Signature:

#![allow(unused)]
fn main() {
pub fn integrate_with_fuel<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    fuel: &Fuel,
) -> Result<Atom<'a>, FuelError>
}

Description: integration with a fuel budget. The integration traversal itself uses an internal depth limit (MAX_DEPTH = 8); this entry point threads fuel into the two simplification phases after integration, preventing pathological results from making the rewrite engine loop indefinitely.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>the expression arena
exprAtom<'a>the integrand
varSymbolthe integration variable
fuel&Fuelthe fuel budget (rewrite step limit)

Returns:

  • Ok(Atom<'a>) — integration simplification succeeded
  • Err(FuelError) — fuel exhausted during simplification

Difference from integrate:

integrate imposes no limit on the simplification phases; integrate_with_fuel applies a fuel constraint to them. The depth limit (MAX_DEPTH) of the integration traversal itself is the same for both.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;
use ocas_core::fuel::Fuel;
use ocas_calc::integrate_with_fuel;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let fuel = Fuel::new(500);
let result = integrate_with_fuel(&ctx, x, Symbol::new("x"), &fuel);
match result {
    Ok(expr) => assert_eq!(expr.to_string(), "(2^-1)*(x^2)"),
    Err(_) => panic!("fuel exhausted"),
}
}

See also: integrate


taylor

Signature:

#![allow(unused)]
fn main() {
pub fn taylor<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    point: Atom<'a>,
    order: usize,
) -> Atom<'a>
}

Description: computes the Taylor expansion of expr in var around point, up to and including order.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>the expression arena
exprAtom<'a>the expression to expand
varSymbolthe expansion variable
pointAtom<'a>the expansion point (any expression, usually 0 or a constant)
orderusizethe expansion order (inclusive)

Returns: Atom<'a> — the truncated polynomial:

$$\sum_{n=0}^{\text{order}} \frac{f^{(n)}(\text{point})}{n!} \cdot (\text{var} - \text{point})^n$$

Algorithm:

Computes each coefficient by repeated symbolic differentiation and evaluation at the expansion point:

  1. For $n = 0, 1, \dots, \text{order}$:
    • Compute $f^{(n)}(x)$ (via diff)
    • Evaluate at $x = \text{point}$ with substitute to obtain $f^{(n)}(\text{point})$
    • Multiply by $\frac{1}{n!}$ (via mul_by_factorial_inverse)
    • Multiply by $(x - \text{point})^n$
  2. Sum all terms and apply the calculus simplification rules

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::taylor;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");

// exp(x) expanded at x=0 to order 3
let expr = ctx.fun("exp", &[x]);
let result = taylor(&ctx, expr, Symbol::new("x"), ctx.num(0), 3);
assert_eq!(
    result.to_string(),
    "1 + x + ((2^-1)*(x^2)) + ((6^-1)*(x^3))"
);

// sin(x) expanded at x=0 to order 5
let expr = ctx.fun("sin", &[x]);
let result = taylor(&ctx, expr, Symbol::new("x"), ctx.num(0), 5);
// Output: x + (-1*(6^-1)*(x^3)) + ((120^-1)*(x^5))
}

See also: diff, substitute


substitute

Signature:

#![allow(unused)]
fn main() {
pub fn substitute<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    replacement: Atom<'a>,
) -> Atom<'a>
}

Description: replaces every occurrence of the variable var in expr with replacement.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>the expression arena
exprAtom<'a>the expression to rewrite
varSymbolthe variable name to replace
replacementAtom<'a>the replacement expression

Returns: Atom<'a> — the new expression after substitution. The original expression is unaffected (immutable data structure on the arena).

Semantics:

  • Traverses the expression tree depth-first
  • Replaces only leaf nodes Var(Symbol) that exactly match var
  • No automatic simplification after substitution (call simplify separately if needed)

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::substitute;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");

// (x² + x)[x → y] = y² + y
let expr = ctx.add(&[ctx.pow(x, ctx.num(2)), x]);
let result = substitute(&ctx, expr, Symbol::new("x"), y);
assert_eq!(result.to_string(), "(y^2) + y");

// sin(x)[x → 2*y] = sin(2*y)
let expr = ctx.fun("sin", &[x]);
let two_y = ctx.mul(&[ctx.num(2), y]);
let result = substitute(&ctx, expr, Symbol::new("x"), two_y);
assert_eq!(result.to_string(), "sin(2*y)");
}

See also: taylor, diff


apart

Signature:

#![allow(unused)]
fn main() {
pub fn apart<D: EuclideanDomain>(
    num: &DenseUnivariatePolynomial<D>,
    den: &DenseUnivariatePolynomial<D>,
) -> (
    Option<DenseUnivariatePolynomial<D>>,
    Vec<PartialFractionTerm<D>>,
)
}

Description: performs partial fraction decomposition of the rational function $\frac{\text{num}(x)}{\text{den}(x)}$.

Parameters:

ParameterTypeDescription
num&DenseUnivariatePolynomial<D>the numerator polynomial
den&DenseUnivariatePolynomial<D>the denominator polynomial

Returns: (Option<poly>, Vec<PartialFractionTerm<D>>) — the polynomial part (when the numerator degree ≥ the denominator degree) and the list of partial fraction terms.

Mathematical background:

Given a proper fraction $\frac{p(x)}{q(x)}$ (i.e., $\deg(p) < \deg(q)$), factor the denominator square-free as $q = \prod_i f_i^{e_i}$, then decompose:

$$\frac{p(x)}{q(x)} = \text{poly_part} + \sum_i \sum_{k=1}^{e_i} \frac{a_{i,k}(x)}{f_i(x)^k}$$

If $\deg(p) \geq \deg(q)$, perform polynomial division first; the quotient becomes poly_part and the remainder is decomposed.

The PartialFractionTerm<D> struct:

#![allow(unused)]
fn main() {
pub struct PartialFractionTerm<D: EuclideanDomain> {
    pub numer: DenseUnivariatePolynomial<D>,  // numerator a_{i,k}(x)
    pub denom: DenseUnivariatePolynomial<D>,  // irreducible factor f_i(x)
    pub exp: usize,                            // exponent k
}
}

Represents the fraction $\frac{\text{numer}(x)}{\text{denom}(x)^{\text{exp}}}$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_calc::partial_fraction::apart;

let d = RationalDomain;

// 1 / (x² - 1) — the denominator is square-free (a single square-free factor x²-1)
let num = DenseUnivariatePolynomial::from_coeffs(d, vec![Rational::new(1, 1)]);
let den = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Rational::new(-1, 1),  // constant term
    Rational::new(0, 1),   // x coefficient
    Rational::new(1, 1),   // x² coefficient
]);
let (poly_part, terms) = apart(&num, &den);
assert!(poly_part.is_none()); // deg(num) < deg(den), no polynomial part
// x²-1 is square-free → a single term with denominator x²-1 and exponent 1
// (apart works on the square-free factorization, not the irreducible
// factorization, so it does not split into (x-1)(x+1))

// (x³ + 1) / (x² - 1) — requires polynomial division
let num = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Rational::new(1, 1), Rational::new(0, 1),
    Rational::new(0, 1), Rational::new(1, 1),
]);
let (poly_part, terms) = apart(&num, &den);
assert!(poly_part.is_some()); // the quotient polynomial x
}

See also: together, rational function integration


together

Signature:

#![allow(unused)]
fn main() {
pub fn together<D: EuclideanDomain>(
    poly_part: Option<&DenseUnivariatePolynomial<D>>,
    terms: &[PartialFractionTerm<D>],
) -> (DenseUnivariatePolynomial<D>, DenseUnivariatePolynomial<D>)
}

Description: recomposes partial fraction terms back into a single rational function. The inverse of apart.

Parameters:

ParameterTypeDescription
poly_partOption<&DenseUnivariatePolynomial<D>>the polynomial part (the first element returned by apart)
terms&[PartialFractionTerm<D>]the list of partial fraction terms

Returns: (numerator, denominator) — the merged numerator and denominator polynomials.

Algorithm:

Combines the polynomial part and all fraction terms over a common denominator: compute the common denominator $\text{lcm}$, expand each term to the common denominator, sum, and return the resulting numerator and denominator.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_calc::partial_fraction::{apart, together};

let d = RationalDomain;
let num = DenseUnivariatePolynomial::from_coeffs(d, vec![Rational::new(1, 1)]);
let den = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Rational::new(-1, 1), Rational::new(0, 1), Rational::new(1, 1),
]);

// apart → round-trip → together should restore the original fraction
let (poly_part, terms) = apart(&num, &den);
let (result_num, result_den) = together(poly_part.as_ref(), &terms);
// result_num/result_den is equivalent to num/den (possibly up to a constant factor)
}

See also: apart


Rational function integration

Source: ocas-calc/src/integral/rational.rs

The rational function integrator integrate_rational handles rational functions of $\var$ (with no other variables or function applications). It follows the standard three-step method from Chapter 2 of Bronstein's Symbolic Integration I:

1. Polynomial part

Integrate $\int \sum c_k x^k , dx = \sum \frac{c_k}{k+1} x^{k+1}$ term by term.

2. Hermite reduction

For a proper fraction $\frac{a(x)}{d(x)}$ ($d$ monic), decompose:

$$\frac{a}{d} = \frac{d}{dx}\left(\frac{g_{\text{num}}}{g_{\text{den}}}\right) + \frac{a_1}{d_1}$$

where $d_1$ is square-free. The derivative of the rational part $\frac{g_{\text{num}}}{g_{\text{den}}}$ can be computed directly.

3. Logarithmic part

For the remaining part $\frac{a_1}{d_1}$ with square-free denominator, choose a strategy by denominator degree:

Denominator degreeStrategy
0constant integration
1$c \cdot \log(ax + b)$
2complete the square → $\log$ or $\text{atan}$
≥ 3Rothstein–Trager resultant method

The Rothstein–Trager method:

Compute $R(t) = \text{Res}_x(d, a - t \cdot d')$; the rational roots $c_i$ of $R(t)$ give:

$$\int \frac{a}{d} = \sum_i c_i \cdot \log(\gcd(d, a - c_i \cdot d'))$$

When $R(t)$ cannot be completely factored over $\mathbb{Q}$, the corresponding terms are returned in unevaluated form Integral(term, var).

Returns:

  • Some(Atom) — the expression is a rational function of $\var$; returns the integration result
  • None — the expression is not a pure rational function of $\var$ (contains other variables or functions)

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::integral::rational::integrate_rational;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");

// ∫ 1/x dx = log(x)
let result = integrate_rational(&ctx, ctx.pow(x, ctx.num(-1)), Symbol::new("x"));
assert_eq!(result.unwrap().to_string(), "log(x)");

// ∫ 1/(x²+1) dx = atan(x) (via completing the square)
// The expression must be built as a rational function of x
}

See also: apart, integrate


Risch algorithm

Source: ocas-calc/src/integral/risch.rs, ocas-calc/src/tower/

The Risch algorithm handles integrals of elementary transcendental functions, based on Chapters 5–6 of Bronstein's Symbolic Integration I.

Differential field tower

The algorithm first constructs the differential field tower $\mathbb{Q}(x, t_1, \dots, t_n)$, where each generator $t_i$ is a logarithm or an exponential:

#![allow(unused)]
fn main() {
pub(crate) enum GenKind {
    Constant,  // a constant symbol (e.g. the imaginary unit I), D t = 0
    Log,       // t_i = log(u)
    Exp,       // t_i = exp(u)
}
}

Tower construction (build_tower) walks the function applications in the expression, creating a new layer for every log or exp. Constraints:

  • Only log and exp are accepted (trigonometric functions must first be rewritten via trig_to_exp)
  • Algebraic functions are rejected (non-integer exponents, e.g. $\sqrt{x}$)
  • Algebraically dependent generators are rejected (e.g., $\log(x)$ and $\log(2x)$)

Layer-by-layer integration

At each tower layer $k(t_i)$, the algorithm performs:

  1. Hermite reduction (hermite_tower): decompose $\frac{a}{d}$ into $D(g) + \frac{a_1}{d_1}$, where $d_1$ is square-free
  2. Polynomial part:
    • Primitive layer ($t = \log(u)$): method of undetermined coefficients; the top-level constant is fixed by the logarithmic constraint
    • Hyperexponential layer ($t = \exp(u)$): solve the Risch differential equation $Dq + fq = g$ at each layer
  3. Logarithmic part: match the logarithmic derivative identity $c \cdot \frac{D d}{d} \to c \cdot \log(d)$

Handling trigonometric functions

integrate first tries Risch on the original expression; when that fails and the integrand contains trigonometric functions, they are rewritten into complex exponential form via trig_to_exp and Risch is attempted again:

$$\sin(u) \to \frac{e^{iu} - e^{-iu}}{2i}, \quad \cos(u) \to \frac{e^{iu} + e^{-iu}}{2}$$

The integration result is then converted back to real form where possible via realify:

Complex patternReal form
$c \cdot \log(u + iv) + c \cdot \log(u - iv)$$c \cdot \log(u^2 + v^2)$
$c \cdot \log(u + iv) - c \cdot \log(u - iv)$$2c \cdot \text{atan}(v/u)$
$\exp(Iu) \cdot \exp(Iv)$$\exp(I(u+v))$ (combines the exponents so cancellation is visible, e.g. $e^{Ix}e^{-Ix} \to e^0$)

If realify cannot match a pattern, the complex form is returned (still mathematically correct; verifiable by differentiation).

Depth limit

MAX_RISCH_DEPTH = 16, enforced via the thread-local counter RISCH_DEPTH and the guard RischDepthGuard (RAII, decrements automatically).

Returns:

  • Some(Atom) — integration succeeded
  • None — the expression is beyond the implemented fragment (the caller falls back to other integrators)

Example:

#![allow(unused)]
fn main() {
// The Risch algorithm is invoked automatically through integrate; usually not called directly
// ∫ exp(x²) · 2x dx = exp(x²) — a typical example the Risch algorithm handles
// ∫ sin(x)/x dx — no elementary antiderivative; Risch returns None and the special function layer handles it
}

See also: integrate, special function integration


Special function integration

Source: ocas-calc/src/integral/special.rs

When the Risch algorithm proves that the integral has no elementary antiderivative, the special function integrator tries to match known non-elementary antiderivatives.

Supported special functions

IntegrandAntiderivativeFunction name
$e^{-x^2}$$\frac{\sqrt{\pi}}{2},\text{erf}(x)$error function
$e^{x^2}$$\frac{\sqrt{\pi}}{2},\text{erfi}(x)$imaginary error function
$\frac{e^x}{x}$$\text{Ei}(x)$exponential integral
$\frac{\sin x}{x}$$\text{Si}(x)$sine integral
$\frac{\cos x}{x}$$\text{Ci}(x)$cosine integral
$\frac{\sinh x}{x}$$\text{Shi}(x)$hyperbolic sine integral
$\frac{\cosh x}{x}$$\text{Chi}(x)$hyperbolic cosine integral
$\sin(x^2)$$\sqrt{\pi/2},\text{fresnels}(\sqrt{2/\pi},x)$Fresnel S integral (function name fresnels)
$\cos(x^2)$$\sqrt{\pi/2},\text{fresnelc}(\sqrt{2/\pi},x)$Fresnel C integral (function name fresnelc)

Matching strategy:

special_integrate examines the factor structure of the integrand:

  • erf_family: matches forms $e^{c \cdot x^2}$
  • ei_family: matches forms $e^{cx} / x$
  • trig_integral_family: matches $\sin(x)/x$, $\cos(x)/x$, $\sinh(x)/x$, $\cosh(x)/x$ (the argument must be exactly $x$)
  • fresnel_family: matches $\sin(cx^2)$, $\cos(cx^2)$
  • reduction_families (0.27.3): the reduction families that turn a polynomial (or a monomial denominator power) times a special head into a closed form, using only identities checked in the module's own tests:
    • polynomial $\times F(a + b x)$ for erf, erfc, erfi, Si, Ci, Shi, Chi, Ei (substitute $u = a + bx$, expand, then integrate by parts with an elementary residual);
    • polynomial $\times \text{Ei}(n, a + bx)$ (integer order $n$), via the inverse recurrence $E_n = (e^{-u} - n E_{n+1})/u$;
    • $F(bx)/x^m$ for $m \ge 2$ and $F = \text{Ei}$ or $\text{Ei}(n, \cdot)$, descending to $\text{Ei}(\pm u)$ plus elementary terms;
    • polynomial $\times F(a + bx)^2$ for the six square-capable heads, via parts plus the Gaussian moment recursion (erf/erfi) or the coupled $F \cdot \cos/\sin$ recursion (Si/Ci/Shi/Chi).

Every reduction chain carries a hard step budget (MAX_SPECIAL_STEPS = 16, MAX_SPECIAL_DEG = 8) and declines rather than guessing; no family ever emits an Integral(...) residue.

Returns:

  • Some(Atom) — matched successfully; returns the antiderivative containing special functions
  • None — no known pattern matches; the caller returns Integral(expr, var)

Design notes:

The special function definitions are consistent with SymPy, and the results can be cross-validated with sympy.integrate. The numerical verification oracle (ocas-tests/src/integral_eval.rs) implements every one of these heads, so solved results in this family are checked by differentiating the emitted form rather than by the absence of a residue.

See also: Risch algorithm, integrate


Helper functions

is_fallback

#![allow(unused)]
fn main() {
pub(crate) fn is_fallback<'a>(atom: &Atom<'a>) -> bool
}

Description: checks whether atom is an unevaluated integral form (function name "Integral"). Note that unevaluated derivatives (the Derivative form) are constructed directly by diff and are not detected by this function.

See also: integrate, diff

is_constant

#![allow(unused)]
fn main() {
pub(crate) fn is_constant<'a>(expr: Atom<'a>, var: Symbol) -> bool
}

Description: checks whether expr does not contain the variable var (i.e., is constant with respect to var).

fraction_exponent

#![allow(unused)]
fn main() {
fn fraction_exponent<'a>(exp: Atom<'a>) -> Option<(i64, i64)>
}

Description: parses an exponent atom into a fraction $p/q$ (small integers). Accepts the form $p \cdot q^{-1}$; used for power-function integration.

linear_form

#![allow(unused)]
fn main() {
fn linear_form<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol)
    -> Option<(Atom<'a>, Atom<'a>)>
}

Description: if expr is of the form $a \cdot \text{var} + b$ ($a, b$ constant with respect to var), returns (a, b); otherwise returns None.


Internal architecture

Simplification pipeline

  • The result of integrate passes through two simplification stages: first the default rewrite rules default_rules (algebraic identities such as $0 + x = x$, $1 \cdot x = x$, $x^1 = x$) for 20 steps, then calculus_rules for 10 steps, and finally normalize to canonical form
  • diff and taylor simplify with calculus_rules directly, then normalize

calculus_rules (rules.rs) builds on default_rules and adds calculus-specific identities: exp(0) → 1, log(1) → 0, sin(0) → 0, cos(0) → 1, tan(0) → 0, $(-1) \cdot (-1) \to 1$, $1^x \to 1$, etc.

Depth limits

LimitValuePurpose
MAX_DEPTH8integrate_raw recursion depth
MAX_RISCH_DEPTH16Risch algorithm recursion depth
PARTS_MAX_DEPTH2integration-by-parts recursion depth

Module dependency graph

ocas-calc
├── derivative::diff
├── integral
│   ├── mod.rs          — integrate, integrate_with_fuel, integrate_heuristic
│   ├── heuristic.rs    — integration by parts, trig/Weierstrass/Euler substitutions
│   ├── rational.rs     — Hermite reduction, Rothstein–Trager
│   ├── risch.rs        — Risch algorithm core
│   ├── rde.rs          — Risch differential equation solver
│   ├── trig.rs         — trig→exponential rewriting, realify
│   ├── special.rs      — erf/Ei/Si/Ci/Fresnel
│   └── tower/
│       ├── build.rs    — differential field tower construction
│       ├── elem.rs     — tower element types (KElem, KPoly, KRat)
│       └── convert.rs  — Atom ↔ tower element conversion
├── series.rs           — taylor, substitute
├── partial_fraction.rs — apart, together
└── rules.rs            — calculus simplification rules

See also

Rust API Reference: Solvers

This chapter covers the equation solving functionality in oCAS, organized into three modules: linear systems and Diophantine equations (ocas-calc::solve), polynomial system solving (ocas-poly::ideal), and ordinary differential equation solving (ocas-calc::ode).


Linear systems

solve_linear_rational

Signature: pub fn solve_linear_rational(a: &[Vec<i64>], b: &[i64]) -> Result<Vec<(i64, i64)>, SolveError>

Description: Solves the linear system $A\mathbf{x} = \mathbf{b}$ over the field of rational numbers $\mathbb{Q}$.

Parameters:

ParameterTypeDescription
a&[Vec<i64>]The $n \times n$ coefficient matrix stored row-major; each Vec<i64> is one row
b&[i64]The right-hand side vector of length $n$

Returns: Result<Vec<(i64, i64)>, SolveError>

  • Ok(vec): the solution vector; each element (num, den) represents the rational number $\frac{\text{num}}{\text{den}}$, with $\text{den} > 0$ and $\gcd(|\text{num}|, \text{den}) = 1$ guaranteed
  • Err(SolveError): see the error list below

Errors:

  • SolveError::EmptySystem — $A$ or $\mathbf{b}$ is empty
  • SolveError::NonSquare — $A$ is not square (or the length of $\mathbf{b}$ does not match the number of rows)
  • SolveError::Inconsistent — the system has no solution (rank of the augmented matrix > rank of the coefficient matrix)
  • SolveError::Underdetermined { rank } — the system has infinitely many solutions (rank deficient)
  • SolveError::ResultNotInDomain — the solution is not in the target domain
  • SolveError::Matrix(_) — an internal matrix operation failed (converted via From<MatrixError>)
  • SolveError::Other(_) — any other error

Example:

#![allow(unused)]
fn main() {
use ocas_calc::solve::solve_linear_rational;

// Solve:
//   2x + y = 5
//   x - y = 1
// → x = 2, y = 1
let a = vec![vec![2, 1], vec![1, -1]];
let b = vec![5, 1];
let x = solve_linear_rational(&a, &b).unwrap();
assert_eq!(x, vec![(2, 1), (1, 1)]);
}

See also: solve_linear_integer, Matrix::solve


solve_linear_integer

Signature: pub fn solve_linear_integer(a: &[Vec<i64>], b: &[i64]) -> Result<Vec<i64>, SolveError>

Description: Solves the linear system $A\mathbf{x} = \mathbf{b}$ over the ring of integers $\mathbb{Z}$. Requires the solution to be integral; otherwise an error is returned.

Parameters:

ParameterTypeDescription
a&[Vec<i64>]The $n \times n$ coefficient matrix stored row-major
b&[i64]The right-hand side vector of length $n$

Returns: Result<Vec<i64>, SolveError>

  • Ok(vec): the integral solution vector
  • Err(SolveError): the error types are the same as for solve_linear_rational, with a possible additional ResultNotInDomain (the solution contains fractions)

Errors: same as solve_linear_rational.

Example:

#![allow(unused)]
fn main() {
use ocas_calc::solve::solve_linear_integer;

// Solve:
//   x + y = 3
//   x - y = 1
// → x = 2, y = 1
let a = vec![vec![1, 1], vec![1, -1]];
let b = vec![3, 1];
let x = solve_linear_integer(&a, &b).unwrap();
assert_eq!(x, vec![2, 1]);
}

See also: solve_linear_rational


Diophantine equations

DiophantineSolution

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiophantineSolution {
    /// Particular solution (x0, y0)
    pub particular: (i64, i64),
    /// Homogeneous solution step (x_step, y_step)
    pub general: (i64, i64),
}
}

Description: Representation of the solutions of the linear Diophantine equation $ax + by = c$.

Fields:

FieldTypeDescription
particular(i64, i64)A particular solution $(x_0, y_0)$
general(i64, i64)A basis solution $(x_{\text{step}}, y_{\text{step}})$ of the homogeneous equation $ax + by = 0$; the general solution is $(x_0 + k \cdot x_{\text{step}},; y_0 + k \cdot y_{\text{step}})$ with $k \in \mathbb{Z}$

solve_diophantine

Signature: pub fn solve_diophantine(a: i64, b: i64, c: i64) -> Option<DiophantineSolution>

Description: Solves the linear Diophantine equation $ax + by = c$ using the extended Euclidean algorithm. A solution exists if and only if $\gcd(a, b) \mid c$.

Parameters:

ParameterTypeDescription
ai64The coefficient of $x$
bi64The coefficient of $y$
ci64The constant on the right-hand side

Returns: Option<DiophantineSolution>

  • Some(sol): the particular/general solution structure when a solution exists
  • None: $\gcd(a, b) \nmid c$; the equation has no integer solution

Errors: does not return a Result; returns None when unsolvable.

Example:

#![allow(unused)]
fn main() {
use ocas_calc::solve::solve_diophantine;

// Solve 3x + 5y = 1
// Particular solution: x = 2, y = -1 (since 3×2 + 5×(-1) = 1)
// General solution: x = 2 + 5k, y = -1 - 3k
let sol = solve_diophantine(3, 5, 1).unwrap();
assert_eq!(sol.particular, (2, -1));
assert_eq!(sol.general, (5, -3));

// No solution: 2x + 4y = 3 (gcd(2,4)=2 does not divide 3)
assert!(solve_diophantine(2, 4, 3).is_none());
}

See also: solve_linear_integer


Polynomial system solving

RealSolution

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct RealSolution {
    /// The value of each variable (in variable order)
    pub values: Vec<f64>,
    /// Algebraic multiplicity
    pub multiplicity: usize,
}
}

Description: A real solution (numerical approximation) of a polynomial system of equations.

Fields:

FieldTypeDescription
valuesVec<f64>The variable values $[x_1, x_2, \dots, x_n]$, in the order of the variables in the original system
multiplicityusizeThe algebraic multiplicity of the solution (the current implementation always sets this to 1; the actual multiplicity is not computed yet)

ZeroDimSolutions

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct ZeroDimSolutions {
    /// All real solutions
    pub solutions: Vec<RealSolution>,
    /// Vector space dimension of the quotient ring k[x₁,...,xₙ]/I
    pub vector_space_dimension: usize,
}
}

Description: The solution result of a zero-dimensional polynomial system.

Fields:

FieldTypeDescription
solutionsVec<RealSolution>All real solutions (may contain approximations)
vector_space_dimensionusizeThe vector space dimension of the quotient ring $k[x_1, \dots, x_n]/I$, equal to the product of the degrees of the univariate polynomials in the Lex GB

PolynomialSystemSolution

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub enum PolynomialSystemSolution {
    /// Zero-dimensional system: finitely many solutions
    ZeroDimensional(ZeroDimSolutions),
    /// Positive-dimensional system: the solution set has a positive-dimensional component; returns a Gröbner basis
    PositiveDimensional(GroebnerBasis<RationalDomain, Lex>),
    /// Inconsistent system (the ideal is ⟨1⟩), no solutions
    Empty,
}
}

Description: The enum type of the result of solving a polynomial system. The system automatically determines the dimension and selects the appropriate solving strategy.

Variants:

VariantDescription
ZeroDimensional(z)The system has finitely many solutions; z contains all real solutions and the vector space dimension
PositiveDimensional(gb)The solution set of the system has a positive-dimensional component (infinitely many solutions); returns a Lex-order Gröbner basis
EmptyThe system is inconsistent (the Gröbner basis is {1}); no solutions. Note: an empty input equation list returns PositiveDimensional (with an empty basis), not Empty

solve_polynomial_system

Signature:

#![allow(unused)]
fn main() {
pub fn solve_polynomial_system(
    equations: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
    algo: Algorithm,
) -> PolynomialSystemSolution
}

Description: Solves the polynomial system $f_1 = f_2 = \cdots = f_m = 0$. Automatically computes a Lex-order Gröbner basis, determines the dimension of the system, and for zero-dimensional systems finds all real solutions by triangular decomposition and back substitution.

Parameters:

ParameterTypeDescription
equations&[SparseMultivariatePolynomial<RationalDomain, Lex>]The polynomial system; each polynomial represents one equation $f_i = 0$
algoAlgorithmGröbner basis algorithm selection: Algorithm::Auto (automatic), Algorithm::Buchberger, Algorithm::F4, Algorithm::F5

Returns: PolynomialSystemSolution

Errors: does not return a Result; all possible outcomes are expressed through the enum variants (zero-dimensional solutions, positive-dimensional basis, empty system).

Algorithm description:

  1. Compute a Gröbner basis of the ideal generated by the equations (using the specified algorithm). Since the input polynomials are already typed in Lex order (SparseMultivariatePolynomial<RationalDomain, Lex>), the GB is computed directly in Lex order; no order change is needed
  2. If the GB is {1}, return Empty (the system is inconsistent, no solutions)
  3. Determine whether it is zero-dimensional: whether each variable $x_i$ has a pure-power leading monomial $x_i^{N_i}$ in the GB
  4. Zero-dimensional: extract the triangular structure of the Lex GB and back-substitute variable by variable starting from the last one (Sturm root isolation + bisection refinement), solving the real roots of the univariate polynomial for each variable; the quotient-ring dimension is estimated as the product of the univariate polynomial degrees
  5. Positive-dimensional: return the Lex GB for further analysis

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, SparseMultivariatePolynomial};
use ocas_poly::ideal::{solve_polynomial_system, PolynomialSystemSolution};

let d = RationalDomain;

// System:
//   x² + y² - 1 = 0  (unit circle)
//   x - y = 0        (line y = x)
// Solutions: (±1/√2, ±1/√2)
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),   // x²
    (vec![0, 2], Rational::new(1, 1)),   // y²
    (vec![0, 0], Rational::new(-1, 1)),  // -1
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),   // x
    (vec![0, 1], Rational::new(-1, 1)),  // -y
]);

let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
match sol {
    PolynomialSystemSolution::ZeroDimensional(z) => {
        assert_eq!(z.solutions.len(), 2);
        // Two solutions: approximately (0.707, 0.707) and (-0.707, -0.707)
    }
    _ => panic!("expected zero-dimensional system"),
}
}

See also: GroebnerBasis, Algorithm, is_zero_dimensional


Ordinary differential equation (ODE) solving

ODE

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy)]
pub struct ODE<'a> {
    /// Equation in standard form lhs - rhs = 0
    pub equation: Atom<'a>,
    /// Unknown function, e.g. y(x)
    pub func: Atom<'a>,
    /// Independent variable, e.g. x
    pub var: Symbol,
}
}

Description: Describes an ordinary differential equation. The equation is stored in the standard form equation = 0, where func is the unknown function (e.g., $y(x)$) and var is the independent variable (e.g., $x$).

Fields:

FieldTypeDescription
equationAtom<'a>The normalized form of the equation (lhs - rhs)
funcAtom<'a>The unknown function, e.g., y(x)
varSymbolThe independent variable symbol, e.g., "x"

Construction example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::ode::ODE;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);

// Represents y' - y = 0
let ode = ODE {
    equation: ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]),
    func: y,
    var: Symbol::new("x"),
};
}

ODESolution

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy)]
pub enum ODESolution<'a> {
    /// Explicit solution y = expr
    Explicit(Atom<'a>),
    /// Implicit solution F(x, y) = 0
    Implicit(Atom<'a>),
    /// Parametric solution (x(t), y(t))
    Parametric(Atom<'a>, Atom<'a>),
    /// Series solution (truncated expression + number of terms)
    Series(Atom<'a>, usize),
    /// System solution (components)
    System(&'a [Atom<'a>]),
    /// Failed to solve; returns the original ODE
    Unsolved(ODE<'a>),
}
}

Description: The enum type of the result of solving an ODE.

Variants:

VariantDescription
Explicit(expr)An explicit solution $y = \text{expr}$, containing free constants C1, C2, ...
Implicit(expr)An implicit solution $F(x, y) = 0$ (stores the expression $F$)
Parametric(x(t), y(t))A parametric solution: two expressions giving $x(t)$ and $y(t)$
Series(expr, n)A series solution; expr is the truncated series expression and n the number of terms
System(components)A system solution; components[i] is the solution for the $i$-th unknown function
Unsolved(ode)No analytic solution found; returns the original ODE

ODEType

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ODEType {
    Separable,
    LinearFirst,
    Bernoulli,
    Exact,
    Homogeneous,
    LinearConstantCoeff,
    CauchyEuler,
    ReductionOfOrder,
    PowerSeries,
}
}

Description: The ODE type enum, which determines which solution method is used.

Variants:

VariantEquation formDescription
Separable$g(y),y' = f(x)$Separable: $\int g(y),dy = \int f(x),dx + C$
LinearFirst$y' + p(x),y = q(x)$First-order linear: integrating factor $\mu = e^{\int p,dx}$
Bernoulli$y' + p(x),y = q(x),y^n$Bernoulli equation: substitution $v = y^{1-n}$ reduces to linear
Exact$M,dx + N,dy = 0$, $\frac{\partial M}{\partial y} = \frac{\partial N}{\partial x}$Exact equation: potential function $F(x,y) = C$
Homogeneous$y' = f(y/x)$Homogeneous equation: substitution $v = y/x$ reduces to separable
LinearConstantCoeff$a,y'' + b,y' + c,y = f(x)$Linear with constant coefficients: characteristic equation + undetermined coefficients (polynomial/exponential/trigonometric forcing)
CauchyEuler$a,x^2,y'' + b,x,y' + c,y = f(x)$Cauchy-Euler: substitution $x = e^t$ reduces to constant coefficients
ReductionOfOrder$a(x),y'' + b(x),y' + c(x),y = f(x)$Reduction of order: tries simple candidate solutions $y_1$; $y_2 = y_1 \int \frac{e^{-\int p,dx}}{y_1^2},dx$; nonzero forcing uses variation of parameters for the particular solution
PowerSerieslinear ODE, ordinary point $x_0 = 0$Power series: $y = \sum_{n=0}^{N-1} a_n x^n$; falls back to the Frobenius method automatically on failure

Note: variation of parameters and the Frobenius method are used internally as solving routines (the former for particular solutions in reduction of order, the latter as the fallback when the power series fails), but they are not separate ODEType variants.


classify_ode

Signature: pub fn classify_ode<'a>(ctx: &'a AtomArena<'a>, ode: ODE<'a>) -> Vec<ODEType>

Description: Analyzes the ODE and returns all applicable solution methods, ordered by priority. The order actually returned (implemented in classify.rs): for first-order equations, LinearFirst → Bernoulli → Separable → Exact → Homogeneous are checked in that order; for second-order (and higher) linear equations, LinearConstantCoeff → CauchyEuler → ReductionOfOrder (second order only); finally, PowerSeries is appended as a fallback for any linear equation of order ≥ 1.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
odeODE<'a>The ODE to classify

Returns: Vec<ODEType> — the list of all applicable solution methods (possibly empty).

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;
use ocas_calc::ode::{classify_ode, ODE, ODEType};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);

// y' - y = 0 is first-order linear
let dy = ctx.fun("Derivative", &[y, x]);
let eq = ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]);
let ode = ODE { equation: eq, func: y, var: Symbol::new("x") };
let methods = classify_ode(&ctx, ode);
assert!(methods.contains(&ODEType::LinearFirst));
}

See also: dsolve, ODEType


dsolve

Signature: pub fn dsolve<'a>(ctx: &'a AtomArena<'a>, ode: ODE<'a>, hint: Option<ODEType>) -> ODESolution<'a>

Description: Solves an ordinary differential equation. Automatically classifies the ODE type and tries the methods in priority order; a specific method can also be requested via hint.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
odeODE<'a>The ODE to solve
hintOption<ODEType>Optional: a specific solution method. When None, the ODE is classified automatically and the methods are tried in priority order

Returns: ODESolution<'a>

Solution strategy (when hint = None):

  1. Call classify_ode to obtain the list of candidate methods
  2. Try each method in priority order
  3. The solution of the first successful method is returned
  4. ODESolution::Unsolved is returned when all methods fail

First-order solvers:

  • Separable: $\int g(y),dy = \int f(x),dx + C$
  • First-order linear: integrating factor $\mu(x) = e^{\int p(x),dx}$, general solution $y = \frac{1}{\mu}\left(\int \mu,q,dx + C\right)$
  • Bernoulli: substitution $v = y^{1-n}$ reduces to first-order linear
  • Exact equations: find a potential function $F$ such that $\frac{\partial F}{\partial x} = M$ and $\frac{\partial F}{\partial y} = N$; when not exact, an integrating factor $\mu(x)$ or $\mu(y)$ is attempted
  • Homogeneous: substitution $v = y/x$ reduces to separable

Second-order solvers:

  • Constant coefficients: characteristic equation $ar^2 + br + c = 0$; build the fundamental solution set according to the discriminant $\Delta = b^2 - 4ac$; find a particular solution by undetermined coefficients
  • Cauchy-Euler: substitution $x = e^t$ converts to a constant-coefficient equation; indicial equation $ar^2 + (b-a)r + c = 0$
  • Reduction of order: tries simple candidate solutions ($1, x, x^2, e^x, e^{-x}, e^{2x}$); once one is found, constructs a second solution via $y_2 = y_1 \int \frac{e^{-\int p,dx}}{y_1^2},dx$
  • Variation of parameters: given fundamental solutions $y_1, y_2$ of the homogeneous equation, finds a particular solution using the Wronskian $W = y_1 y_2' - y_1' y_2$

Series solvers (dispatched by PowerSeries, fixed expansion point $x_0 = 0$, 8 terms):

  • Power series: expand $y = \sum_{n=0}^{N-1} a_n x^n$ at the ordinary point $x_0 = 0$; substitute into the ODE and recurse to determine the coefficients
  • Frobenius (internal fallback): when the power series is not applicable at the ordinary point, falls back to the regular singular point method, expanding $y = x^r \sum_{n=0}^{N-1} a_n x^n$; the indicial equation determines $r$

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::ode::{dsolve, ODE, ODESolution};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);

// Solve y' - y = 0 → C1*exp(x)
let ode = ODE {
    equation: ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]),
    func: y,
    var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
assert!(matches!(sol, ODESolution::Explicit(_)));

// Specify a method
let sol_hint = dsolve(&ctx, ode, Some(ODEType::LinearFirst));
assert!(matches!(sol_hint, ODESolution::Explicit(_)));
}

See also: classify_ode, dsolve_ivp, dsolve_system


dsolve_ivp

Signature:

#![allow(unused)]
fn main() {
pub fn dsolve_ivp<'a>(
    ctx: &'a AtomArena<'a>,
    ode: ODE<'a>,
    y0: Atom<'a>,
    y1: Option<Atom<'a>>,
) -> ODESolution<'a>
}

Description: Solves the initial value problem (IVP) for first- or second-order linear constant-coefficient ODEs using the Laplace transform. The result is an explicit solution without free constants.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
odeODE<'a>The ODE (must be linear with constant coefficients)
y0Atom<'a>The initial condition $y(0)$
y1Option<Atom<'a>>$y'(0)$ for second-order problems; ignored for first-order problems

Returns: ODESolution<'a>

  • Explicit(expr): an explicit solution without free constants
  • Unsolved: the Laplace transform method is not applicable or the inverse cannot be computed

Method description:

  1. Take the Laplace transform of both sides of the ODE, using $\mathcal{L}{y'} = sY - y(0)$ and $\mathcal{L}{y''} = s^2 Y - sy(0) - y'(0)$
  2. Substitute the initial conditions to obtain an algebraic equation for $Y(s)$
  3. Solve for $Y(s)$
  4. Perform the inverse Laplace transform via partial fraction decomposition + lookup of standard pairs

Supported forcing terms: polynomials, exponentials $e^{kx}$, sines/cosines $\sin(\omega x)$/$\cos(\omega x)$, and linear combinations of $e^{kx}\sin(\omega x)$/$e^{kx}\cos(\omega x)$.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::ode::{dsolve_ivp, ODE, ODESolution};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);

// y' - y = 0, y(0) = 1  →  y = exp(x)
let ode = ODE {
    equation: ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]),
    func: y,
    var: Symbol::new("x"),
};
let sol = dsolve_ivp(&ctx, ode, ctx.num(1), None);
assert!(matches!(sol, ODESolution::Explicit(_)));
}

See also: dsolve


dsolve_system

Signature:

#![allow(unused)]
fn main() {
pub fn dsolve_system<'a>(
    ctx: &'a AtomArena<'a>,
    equations: &[Atom<'a>],
    funcs: &[Atom<'a>],
    var: Symbol,
) -> ODESolution<'a>
}

Description: Solves a $2 \times 2$ constant-coefficient linear ODE system $\mathbf{Y}' = A\mathbf{Y}$. The general solution is obtained via eigenvalue decomposition.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
equations&[Atom<'a>]The list of equations, each of the form Derivative(y_i, x) - (a_i1*y1 + a_i2*y2) = 0
funcs&[Atom<'a>]The list of unknown functions, e.g., [y1(x), y2(x)]
varSymbolThe independent variable symbol

Returns: ODESolution<'a>

  • System(&[Atom]): the general solution of each component (containing free constants C1, C2)
  • Unsolved: the system is not supported (not 2×2, or the eigenvalues have no closed form)

Supported cases:

  • Distinct real eigenvalues $\lambda_1 \neq \lambda_2$: $\mathbf{Y} = C_1 \mathbf{v}_1 e^{\lambda_1 x} + C_2 \mathbf{v}_2 e^{\lambda_2 x}$
  • Repeated real eigenvalues (with a generalized eigenvector): $\mathbf{Y} = (C_1 \mathbf{v} + C_2(\mathbf{w} + x\mathbf{v}))e^{\lambda x}$
  • Conjugate complex eigenvalues $\alpha \pm \beta i$: real-valued fundamental solutions such as $e^{\alpha x}(\mathbf{p}\cos\beta x - \mathbf{q}\sin\beta x)$

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::ode::{dsolve_system, ODESolution};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y1 = ctx.fun("y1", &[x]);
let y2 = ctx.fun("y2", &[x]);
let dy1 = ctx.fun("Derivative", &[y1, x]);
let dy2 = ctx.fun("Derivative", &[y2, x]);

// Harmonic oscillator: y1' = y2, y2' = -y1
let eq1 = ctx.add(&[dy1, ctx.mul(&[ctx.num(-1), y2])]);
let eq2 = ctx.add(&[dy2, y1]);
let sol = dsolve_system(&ctx, &[eq1, eq2], &[y1, y2], Symbol::new("x"));
assert!(matches!(sol, ODESolution::System(_)));
}

See also: dsolve


SolveError

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SolveError {
    /// The system of equations is empty
    EmptySystem,
    /// The system is nonlinear
    NonLinear,
    /// The coefficient matrix is not square (or the equation count does not match the unknown count)
    NonSquare,
    /// The system is inconsistent (no solution)
    Inconsistent,
    /// The system is underdetermined (infinitely many solutions)
    Underdetermined { rank: usize },
    /// The result is not in the target domain
    ResultNotInDomain,
    /// An internal matrix operation failed
    Matrix(MatrixError),
    /// Other error with a description
    Other(String),
}
}

Description: Errors that can occur during equation solving. Implements the Display and Error traits. Implements From<MatrixError>: ShapeMismatch → NonSquare, Inconsistent → Inconsistent, Underdetermined { rank } → Underdetermined { rank }, ResultNotInDomain → ResultNotInDomain, RightHandSideIsNotVector → Other("right-hand side is not a vector").

Variants:

VariantDescription
EmptySystemThe input system of equations is empty
NonLinearThe system is nonlinear in the target variables
NonSquareThe coefficient matrix is not square (equation count ≠ unknown count)
InconsistentThe rank of the augmented matrix is greater than the rank of the coefficient matrix
Underdetermined { rank }The rank of the coefficient matrix is less than the number of variables; rank is the actual rank
ResultNotInDomainThe solution is not in the target number domain (e.g., an integral solution was requested but the solution contains fractions)
Matrix(MatrixError)An internal matrix operation failed (converted automatically via From<MatrixError>)
Other(String)Other error with a description

Example:

#![allow(unused)]
fn main() {
use ocas_calc::solve::{solve_linear_rational, SolveError};

// Inconsistent system: x + y = 1, x + y = 2
let a = vec![vec![1, 1], vec![1, 1]];
let b = vec![1, 2];
let result = solve_linear_rational(&a, &b);
assert_eq!(result, Err(SolveError::Inconsistent));
}

Helper functions

substitute_solution_collected

Signature:

#![allow(unused)]
fn main() {
pub fn substitute_solution_collected<'a>(
    ctx: &'a AtomArena<'a>,
    equation: Atom<'a>,
    func: Atom<'a>,
    sol: Atom<'a>,
    var: Symbol,
) -> Atom<'a>
}

Description: Substitutes a candidate solution into the ODE equation and returns the residual after collecting like terms. A zero residual means the candidate solution satisfies the ODE. Mainly used for verification.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
equationAtom<'a>The ODE equation (in lhs - rhs form)
funcAtom<'a>The unknown function (e.g., y(x))
solAtom<'a>The candidate solution expression
varSymbolThe independent variable symbol

Returns: Atom<'a> — the residual expression after substitution and simplification. If zero, the candidate solution is correct.


Source modules

ModulePathContents
Linear solvingocas-calc/src/solve.rssolve_linear_rational, solve_linear_integer, solve_diophantine, SolveError
Polynomial systemsocas-poly/src/ideal.rssolve_polynomial_system, PolynomialSystemSolution, RealSolution, ZeroDimSolutions
ODE classificationocas-calc/src/ode/classify.rsclassify_ode, ODEType
First-order ODEsocas-calc/src/ode/first_order.rsseparable, linear, Bernoulli, exact, and homogeneous solvers
Second-order ODEsocas-calc/src/ode/second_order.rsconstant-coefficient, Cauchy-Euler, reduction-of-order, and variation-of-parameters solvers
Series solutions for ODEsocas-calc/src/ode/series.rspower series and Frobenius solvers
Laplace transforms for ODEsocas-calc/src/ode/laplace.rsthe Laplace transform implementation behind dsolve_ivp
ODE systemsocas-calc/src/ode/systems.rsthe eigenvalue decomposition implementation behind dsolve_system

Rust API Reference: Rewrite & Simplification

This chapter documents the full public API of the ocas-rewrite crate. Built on top of ocas-atom, it provides:

  • Pattern matching: wildcards (_, __, ___) with AC (associative-commutative) backtracking matching
  • Rewrite rules: the Rule abstraction pairing a pattern with a replacement closure
  • Simplification engine: simplify / simplify_with_fuel apply rules iteratively until a fixed point
  • Replacement: replace_once / replace_all / replace_all_multiple controlled replacement
  • Transformation: transform bottom-up traversal, partition_expr combinatorial partitioning
  • E-graphs: equality saturation simplification based on the egg crate (requires the egg feature)

Module layout:

ocas_rewrite
├── pattern      — Pattern, WildcardLevel, PatternAlloc
├── matcher      — match_pattern, Bindings, MatchValue, MatchError
├── rules        — Rule, default_rules, 9 built-in rules
├── simplify     — simplify, simplify_with_fuel
├── replace      — replace_once, replace_all, replace_all_multiple, Replacement, Condition
├── transformer  — transform, partition_expr
├── combinatorics — partitions, PartitionSolution
└── egraph       — AtomLanguage, simplify_with_egraph (feature = "egg")

Pattern System

WildcardLevel

#![allow(unused)]
fn main() {
pub enum WildcardLevel {
    Single,       // x_  — matches a single atom
    Sequence,     // __x — matches 1 or more atoms (in an Add/Mul/Fun argument list)
    NullSequence, // ___x — matches 0 or more atoms
}
}

Function: Defines the matching scope of a wildcard.

VariantDescriptionNaming convention
SingleMatches exactly one atomVariable name ends with _, e.g. x_
SequenceMatches one or more atoms (non-empty sequence)Variable name starts with __, e.g. __x
NullSequenceMatches zero or more atoms (nullable sequence)Variable name ends with ___, e.g. x___

See also: Pattern, match_pattern


Pattern

#![allow(unused)]
fn main() {
pub enum Pattern<'a> {
    Literal(Atom<'a>),
    Wildcard(Symbol, WildcardLevel),
    Add(Vec<Pattern<'a>>),
    Mul(Vec<Pattern<'a>>),
    Pow(Box<(Pattern<'a>, Pattern<'a>)>),
    Fun(Symbol, Vec<Pattern<'a>>),
}
}

Function: A pattern AST that mirrors the Atom structure but adds wildcard nodes.

VariantDescription
Literal(Atom)Matches the given atom exactly
Wildcard(name, level)Wildcard, matched per WildcardLevel and bound to name
Add(pats)Matches an addition node; arguments matched by pats (AC matching)
Mul(pats)Matches a multiplication node; arguments matched by pats (AC matching)
Pow(Box<(base, exp)>)Matches an exponentiation node
Fun(head, pats)Matches a function application; head is the function name, arguments matched in order

Pattern::from_atom

#![allow(unused)]
fn main() {
pub fn from_atom(_ctx: &'a impl PatternAlloc<'a>, atom: Atom<'a>) -> Pattern<'a>
}

Function: Converts an Atom into a Pattern. Variable names ending or starting with _, __, or ___ are automatically recognized as wildcards of the corresponding level (e.g. x_, __x, x___).

Parameters:

ParameterTypeDescription
ctx&impl PatternAlloc<'a>The pattern allocator (pass &())
atomAtom<'a>The expression to convert

Return value: Pattern<'a> — the corresponding pattern.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_rewrite::pattern::{Pattern, WildcardLevel};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let atom = ctx.var("x_");
let pat = Pattern::from_atom(&(), atom);
// pat == Pattern::Wildcard(Symbol::new("x"), WildcardLevel::Single)
}

See also: WildcardLevel, match_pattern


PatternAlloc

#![allow(unused)]
fn main() {
pub trait PatternAlloc<'a> {
    fn alloc_slice(&self, items: &[Pattern<'a>]) -> &'a [Pattern<'a>];
}
}

Function: Helper trait for allocating pattern slices in a caller-owned scratch arena, avoiding leaks into the global arena. The () implementation simply returns the slice via Box::leak (fine for one-off examples).

See also: Rule, default_rules


Pattern Matching

MatchValue

#![allow(unused)]
fn main() {
pub enum MatchValue<'a> {
    Single(Atom<'a>),
    Sequence(&'a [Atom<'a>]),
}
}

Function: The value of a wildcard binding.

VariantDescription
Single(Atom)A single wildcard (_) bound to one atom
Sequence(&[Atom])A sequence wildcard (__ or ___) bound to a slice of atoms

See also: Bindings


Bindings

#![allow(unused)]
fn main() {
pub struct Bindings<'a> { /* private fields */ }
}

Function: The set of wildcard bindings produced by a successful pattern match.

Bindings::new

#![allow(unused)]
fn main() {
pub fn new() -> Self
}

Function: Creates an empty binding set.

Bindings::get

#![allow(unused)]
fn main() {
pub fn get(&self, name: Symbol) -> Option<&MatchValue<'a>>
}

Function: Looks up a bound value by name.

Parameters:

ParameterTypeDescription
nameSymbolThe wildcard name (without trailing underscores)

Return value: Option<&MatchValue<'a>> — a reference to the bound value if present.

Example:

#![allow(unused)]
fn main() {
use ocas_rewrite::matcher::Bindings;

let bindings = Bindings::new();
// Usually produced by match_pattern
}

See also: match_pattern, MatchValue


MatchError

#![allow(unused)]
fn main() {
pub enum MatchError {
    NoMatch,
    InconsistentBinding,
    BudgetExhausted,
}
}

Function: The reason a pattern match failed.

VariantDescription
NoMatchThe pattern structure does not match
InconsistentBindingThe same-named wildcard is bound to different values
BudgetExhaustedAC backtracking exceeded the budget (10,000 by default)

Implements the Display and Error traits.

See also: match_pattern_with_budget


match_pattern

#![allow(unused)]
fn main() {
pub fn match_pattern<'a>(
    pattern: Pattern<'a>,
    atom: Atom<'a>,
) -> Result<Bindings<'a>, MatchError>
}

Function: Matches a pattern against an atom and returns the binding set. Uses the default backtracking budget (10,000 backtracks).

Parameters:

ParameterTypeDescription
patternPattern<'a>The pattern to match
atomAtom<'a>The target expression

Return value:

  • Ok(Bindings) — match succeeded, containing all wildcard bindings
  • Err(MatchError) — match failed

Matching semantics:

  • Add/Mul nodes use AC (associative-commutative) matching: argument order does not matter, associativity is handled automatically
  • Fun nodes match arguments in order, with sequence wildcard support
  • Same-named wildcards must bind to the same value

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_atom::Symbol;
use ocas_core::arena::Arena;
use ocas_rewrite::pattern::{Pattern, WildcardLevel};
use ocas_rewrite::matcher::{match_pattern, MatchValue};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

let pat = Pattern::Add(vec![
    Pattern::Wildcard(Symbol::new("x"), WildcardLevel::Single),
    Pattern::Literal(ctx.num(0)),
]);

let expr = ctx.add(&[ctx.var("y"), ctx.num(0)]);
let bindings = match_pattern(pat, expr).unwrap();
let x = bindings.get(Symbol::new("x")).unwrap();
// MatchValue::Single(Atom("y"))
}

See also: match_pattern_with_budget, Pattern, Bindings


match_pattern_with_budget

#![allow(unused)]
fn main() {
pub fn match_pattern_with_budget<'a>(
    pattern: Pattern<'a>,
    atom: Atom<'a>,
    max_backtracks: usize,
) -> Result<Bindings<'a>, MatchError>
}

Function: Pattern matching with a custom backtracking budget. Suitable for patterns with higher complexity that need a larger search space.

Parameters:

ParameterTypeDescription
patternPattern<'a>The pattern to match
atomAtom<'a>The target expression
max_backtracksusizeThe maximum number of backtracks

Return value: Same as match_pattern.

Errors: Returns MatchError::BudgetExhausted when the budget is exhausted.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_atom::Symbol;
use ocas_core::arena::Arena;
use ocas_rewrite::matcher::match_pattern_with_budget;
use ocas_rewrite::pattern::{Pattern, WildcardLevel};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

let pat = Pattern::Wildcard(Symbol::new("x"), WildcardLevel::Single);
let atom = ctx.var("y");

// Use a larger budget for complex patterns
let bindings = match_pattern_with_budget(pat, atom, 100_000).unwrap();
assert!(bindings.get(Symbol::new("x")).is_some());
}

See also: match_pattern, DEFAULT_MAX_BACKTRACKS


DEFAULT_MAX_BACKTRACKS

#![allow(unused)]
fn main() {
pub const DEFAULT_MAX_BACKTRACKS: usize = 10_000;
}

Function: The default backtracking budget constant used by match_pattern.


Rewrite Rules

Rule

#![allow(unused)]
fn main() {
pub struct Rule<'a> { /* private fields */ }
}

Function: A rewrite rule pairing a pattern with a replacement closure. When the pattern matches a subexpression, the replacement closure receives the bindings and produces a new atom.

Rule::new

#![allow(unused)]
fn main() {
pub fn new<F>(pattern: Pattern<'a>, replacement: F) -> Self
where
    F: Fn(&Bindings<'a>, &AtomArena<'a>) -> Atom<'a> + 'a,
}

Function: Creates a rule from a pattern and a replacement closure.

Parameters:

ParameterTypeDescription
patternPattern<'a>The matching pattern
replacementFn(&Bindings, &AtomArena) -> AtomThe replacement closure, receiving bindings and the arena context

Return value: Rule<'a> — an unconditional rule.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_atom::Symbol;
use ocas_core::arena::Arena;
use ocas_rewrite::matcher::{Bindings, MatchValue};
use ocas_rewrite::pattern::{Pattern, WildcardLevel};
use ocas_rewrite::rules::Rule;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let pat = Pattern::Wildcard(Symbol::new("x"), WildcardLevel::Single);
let rule = Rule::new(pat, |bindings: &Bindings, _ctx: &AtomArena| {
    let x = bindings.get(Symbol::new("x")).unwrap();
    let MatchValue::Single(v) = x else { panic!("expected single"); };
    _ctx.mul(&[_ctx.num(2), *v])
});

let y = ctx.var("y");
let result = rule.apply(&ctx, y).unwrap();
assert_eq!(result.to_string(), "2*y");
}

Rule::with_condition

#![allow(unused)]
fn main() {
pub fn with_condition<F>(self, condition: F) -> Self
where
    F: Fn(&Bindings<'a>) -> bool + 'a,
}

Function: Adds a precondition to the rule. The condition is evaluated after a successful match, before the replacement.

Parameters:

ParameterTypeDescription
conditionFn(&Bindings) -> boolThe condition closure; the replacement runs only when it returns true

Return value: Rule<'a> — a rule with a condition.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_atom::Symbol;
use ocas_core::arena::Arena;
use ocas_rewrite::matcher::{Bindings, MatchValue};
use ocas_rewrite::pattern::{Pattern, WildcardLevel};
use ocas_rewrite::rules::Rule;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let pat = Pattern::Wildcard(Symbol::new("x"), WildcardLevel::Single);
let rule = Rule::new(pat, |_bindings: &Bindings, ctx: &AtomArena| {
    ctx.num(99)
}).with_condition(|bindings: &Bindings| {
    let x = bindings.get(Symbol::new("x")).unwrap();
    let MatchValue::Single(v) = x else { return false; };
    v.to_string() == "y"
});

let y = ctx.var("y");
let z = ctx.var("z");
assert_eq!(rule.apply(&ctx, y).unwrap().to_string(), "99");
assert!(rule.apply(&ctx, z).is_none());
}

Rule::apply

#![allow(unused)]
fn main() {
pub fn apply(&self, ctx: &AtomArena<'a>, atom: Atom<'a>) -> Option<Atom<'a>>
}

Function: Attempts to apply the rule to an atom. Returns Some(replacement result) when the pattern matches and the condition holds, otherwise None.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
atomAtom<'a>The target expression

Return value: Option<Atom<'a>> — Some means the rule was applied, None means no match or the condition failed.

See also: match_pattern, simplify


default_rules

#![allow(unused)]
fn main() {
pub fn default_rules<'a>(
    ctx: &'a AtomArena<'a>,
    alloc: &'a impl PatternAlloc<'a>,
) -> Vec<Rule<'a>>
}

Function: Returns the built-in set of algebraic simplification rules (9 rules in total).

Built-in rule list:

RuleRewriteDescription
add_zerox + 0 → xAdditive identity
add_zero_left0 + x → xAdditive identity (left)
mul_zerox × 0 → 0Multiplicative zero
mul_zero_left0 × x → 0Multiplicative zero (left)
mul_onex × 1 → xMultiplicative identity
mul_one_left1 × x → xMultiplicative identity (left)
add_samex + x → 2xCombine like terms
pow_zerox⁰ → 1Zero-th power
pow_onex¹ → xFirst power

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
alloc&impl PatternAlloc<'a>The pattern allocator (pass &())

Return value: Vec<Rule<'a>> — the rule list.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_rewrite::rules::default_rules;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let rules = default_rules(&ctx, &());
// rules.len() == 9
}

See also: simplify, Rule


Built-in rule functions

Each of the following functions returns a single built-in rule and can be used individually or combined into custom rule sets:

#![allow(unused)]
fn main() {
pub fn add_zero<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn add_zero_left<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn mul_zero<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn mul_zero_left<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn mul_one<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn mul_one_left<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn add_same<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn pow_zero<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
pub fn pow_one<'a>(ctx: &'a AtomArena<'a>, alloc: &'a impl PatternAlloc<'a>) -> Rule<'a>
}

Each rule has the same signature and usage: pass ctx and alloc, get back a Rule<'a>.

Example (using add_same alone):

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_rewrite::rules::add_same;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.add(&[x, x]);
let rule = add_same(&ctx, &());
let result = rule.apply(&ctx, expr).unwrap();
assert_eq!(result.to_string(), "2*x");
}

Simplification Engine

simplify

#![allow(unused)]
fn main() {
pub fn simplify<'a>(
    ctx: &'a AtomArena<'a>,
    atom: Atom<'a>,
    rules: &[Rule<'a>],
    iter_limit: usize,
) -> Atom<'a>
}

Function: Applies the rewrite rules to the expression repeatedly, traversing bottom-up, until a fixed point or the iteration limit is reached. This is the core entry point of the oCAS simplification engine.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
atomAtom<'a>The expression to simplify
rules&[Rule<'a>]The list of rewrite rules
iter_limitusizeThe maximum number of iterations (10–20 recommended)

Return value: Atom<'a> — the simplified expression (registered in the arena).

Behavior:

  1. Each iteration traverses the expression tree bottom-up
  2. All rules are tried in turn on each node
  3. If any rule fires, the round produced a change and the next round runs
  4. Stops when no rule fires or when iter_limit is reached

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_rewrite::rules::default_rules;
use ocas_rewrite::simplify::simplify;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let rules = default_rules(&ctx, &());

let x = ctx.var("x");
let expr = ctx.mul(&[x, ctx.num(0)]);
let result = simplify(&ctx, expr, &rules, 10);
assert_eq!(result.to_string(), "0");
}

See also: simplify_with_fuel, default_rules, Rule


simplify_with_fuel

#![allow(unused)]
fn main() {
pub fn simplify_with_fuel<'a>(
    ctx: &'a AtomArena<'a>,
    atom: Atom<'a>,
    rules: &[Rule<'a>],
    iter_limit: usize,
    fuel: &Fuel,
) -> Result<Atom<'a>, OcasError>
}

Function: Simplification with a fuel budget. Each bottom-up traversal round consumes one fuel unit; when the fuel runs out, the operation stops early.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
atomAtom<'a>The expression to simplify
rules&[Rule<'a>]The list of rewrite rules
iter_limitusizeThe maximum number of iterations
fuel&FuelThe fuel budget (Fuel::default() is effectively unlimited)

Return value:

  • Ok(Atom) — simplification complete (fixed point reached)
  • Err(OcasError::OutOfFuel) — fuel exhausted (no expression is returned in this case; callers must save intermediate results themselves)

Purpose: Prevents pathological inputs from making the simplification engine loop forever. Participates in fuel accounting within nested call chains.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_core::fuel::Fuel;
use ocas_rewrite::rules::default_rules;
use ocas_rewrite::simplify::simplify_with_fuel;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.var("x");
let rules = default_rules(&ctx, &());
let fuel = Fuel::new(100);
let result = simplify_with_fuel(&ctx, expr, &rules, 20, &fuel);
match result {
    Ok(e) => println!("simplified: {}", e),
    Err(_) => println!("fuel exhausted"),
}
}

See also: simplify, Fuel (from the ocas-core crate)


Replacement

replace_once

#![allow(unused)]
fn main() {
pub fn replace_once<'a, F>(
    ctx: &'a AtomArena<'a>,
    atom: Atom<'a>,
    pattern: Pattern<'a>,
    replacement: F,
) -> Atom<'a>
where
    F: Fn(&Bindings<'a>, &AtomArena<'a>) -> Atom<'a>,
}

Function: Traverses top-down and replaces the first matching subexpression, then stops.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
atomAtom<'a>The target expression
patternPattern<'a>The matching pattern
replacementFn(&Bindings, &AtomArena) -> AtomThe replacement closure

Return value: Atom<'a> — the replaced expression (returned unchanged if nothing matches).

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_atom::Symbol;
use ocas_core::arena::Arena;
use ocas_rewrite::pattern::{Pattern, WildcardLevel};
use ocas_rewrite::replace::replace_once;
use ocas_rewrite::matcher::MatchValue;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let pat = Pattern::Wildcard(Symbol::new("x"), WildcardLevel::Single);
let expr = ctx.add(&[ctx.var("a"), ctx.var("b")]);

let result = replace_once(&ctx, expr, pat, |bindings, ctx| {
    let x = bindings.get(Symbol::new("x")).unwrap();
    let MatchValue::Single(v) = x else { panic!() };
    ctx.mul(&[ctx.num(2), *v])
});
// The first matching subexpression is replaced by 2*a
}

See also: replace_all, replace_all_multiple


replace_all

#![allow(unused)]
fn main() {
pub fn replace_all<'a, F>(
    ctx: &'a AtomArena<'a>,
    atom: Atom<'a>,
    pattern: Pattern<'a>,
    replacement: F,
) -> Atom<'a>
where
    F: Fn(&Bindings<'a>, &AtomArena<'a>) -> Atom<'a>,
}

Function: Traverses top-down and replaces all matching subexpressions. Does not perform nested replacement (i.e., replacement results do not participate in further matching).

Parameters: Same as replace_once.

Return value: Atom<'a> — the expression with all matches replaced.

See also: replace_once, replace_all_multiple


replace_all_multiple

#![allow(unused)]
fn main() {
pub fn replace_all_multiple<'a, F>(
    ctx: &'a AtomArena<'a>,
    atom: Atom<'a>,
    replacements: &[Replacement<'a, F>],
) -> Atom<'a>
where
    F: Fn(&Bindings<'a>, &AtomArena<'a>) -> Atom<'a>,
}

Function: Uses multiple replacement rules, tried in order; the first matching rule takes effect. Traverses all nodes top-down.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
atomAtom<'a>The target expression
replacements&[Replacement<'a, F>]The list of replacement rules (ordered by priority)

Return value: Atom<'a> — the replaced expression.

See also: Replacement, replace_all


Replacement

#![allow(unused)]
fn main() {
pub struct Replacement<'a, F>
where
    F: Fn(&Bindings<'a>, &AtomArena<'a>) -> Atom<'a>,
{
    pub pattern: Pattern<'a>,
    pub replacement: F,
    pub condition: Option<Condition<'a>>,
}
}

Function: A single replacement rule, containing a pattern, a replacement closure, and an optional condition.

Fields:

FieldTypeDescription
patternPattern<'a>The matching pattern
replacementFThe replacement closure
conditionOption<Condition<'a>>The optional precondition

See also: replace_all_multiple, Condition


Condition

#![allow(unused)]
fn main() {
pub enum Condition<'a> {
    Predicate(Arc<dyn Fn(&Bindings<'a>) -> bool + 'a>),
}
}

Function: A replacement condition; currently only a predicate form is supported.

Condition::new

#![allow(unused)]
fn main() {
pub fn new<F: Fn(&Bindings<'a>) -> bool + 'a>(f: F) -> Self
}

Function: Creates a condition from a closure.

Example:

#![allow(unused)]
fn main() {
use ocas_rewrite::replace::Condition;

let cond = Condition::new(|bindings| {
    // Replace only when the bound variable x is positive
    true
});
}

See also: Replacement, Rule::with_condition


ReplaceSettings

#![allow(unused)]
fn main() {
pub struct ReplaceSettings {
    pub once: bool,
    pub bottom_up: bool,
    pub nested: bool,
}
}

Function: Replacement traversal settings.

FieldTypeDefaultDescription
onceboolfalseWhether to replace only the first match
bottom_upboolfalseWhether to traverse bottom-up (default is top-down)
nestedboolfalseWhether to continue replacing inside already-replaced subexpressions

See also: replace_all


Transformation

transform

#![allow(unused)]
fn main() {
pub fn transform<'a, F>(
    ctx: &'a AtomArena<'a>,
    atom: Atom<'a>,
    f: F,
) -> Atom<'a>
where
    F: FnMut(Atom<'a>) -> Option<Atom<'a>>,
}

Function: Traverses the expression tree bottom-up and calls the closure f on each node. When the closure returns Some(atom), the node is replaced; when it returns None, the original node is kept (with transformed subexpressions).

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
atomAtom<'a>The target expression
fFnMut(Atom) -> Option<Atom>The transformation closure

Return value: Atom<'a> — the transformed expression.

Behavior:

  1. Recursively traverses the subexpressions (leaf nodes are returned directly)
  2. Rebuilds the current node with the transformed subexpressions
  3. Calls f on the rebuilt node
  4. If f returns Some, the new node is used; if None, the rebuilt node is used

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{Atom, AtomArena, AtomNode};
use ocas_core::arena::Arena;
use ocas_rewrite::transformer::transform;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y]);

let result = transform(&ctx, sum, |a| {
    if let AtomNode::Add(args) = a.node() {
        if args.len() == 2 && args[0] == x && args[1] == y {
            return Some(ctx.add(&[y, x]));
        }
    }
    None
});

assert_eq!(result.to_string(), "y + x");
}

See also: simplify, partition_expr


partition_expr

#![allow(unused)]
fn main() {
pub fn partition_expr<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    bins: &[(Symbol, usize)],
    fill_last: bool,
    repeat: bool,
) -> Atom<'a>
}

Function: Partitions an arg(a₁, a₂, …, aₙ) expression into named bins, returning the sum of products $\sum \text{coeff} \cdot f_1(\cdots) \cdot f_2(\cdots) \cdots$. The parameter design mirrors Symbolica's Transformer::Partition.

Parameters:

ParameterTypeDescription
ctx&AtomArena<'a>The expression arena context
exprAtom<'a>The input expression (must be of the form arg(...))
bins&[(Symbol, usize)]The bin list, each element (function name, capacity)
fill_lastboolWhether surplus elements are absorbed into the last bin
repeatboolWhether the bin pattern repeats until all elements are consumed

Return value: Atom<'a> — the sum of partition results; ctx.num(0) when no valid partition exists; the input expr is returned unchanged when it is not an arg(...) application or an argument is not numeric.

Behavior:

  • The input must be an arg(...) function application with numeric arguments
  • Enumerates all legal assignment ways
  • The coefficient of each assignment is the multinomial coefficient $\binom{n}{k_1, k_2, \dots}$

Example:

#![allow(unused)]
fn main() {
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;
use ocas_rewrite::transformer::partition_expr;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let args = ctx.fun("arg", &[ctx.num(1), ctx.num(2), ctx.num(3)]);
let bins = &[(Symbol::new("f"), 2), (Symbol::new("g"), 1)];
let result = partition_expr(&ctx, args, bins, false, false);
// Returns Σ coeff · f(…, …) · g(…)
}

See also: transform, combinatorics::partitions


Combinatorics Helpers

partitions

#![allow(unused)]
fn main() {
pub fn partitions<T, B>(
    elements: &[T],
    bins: &[(B, usize)],
    fill_last: bool,
    repeat: bool,
) -> Vec<PartitionSolution<T, B>>
where
    T: Clone + Ord + Hash,
    B: Clone + Ord + Hash,
}

Function: Enumerates all ways to partition elements into bins.

Parameters:

ParameterTypeDescription
elements&[T]The elements to distribute
bins&[(B, usize)]The bin list (name, capacity)
fill_lastboolWhether surplus elements are absorbed into the last bin
repeatboolWhether the bin pattern repeats until the elements are exhausted

Return value: Vec<PartitionSolution<T, B>> — all legal partitions.

See also: partition_expr, PartitionSolution


PartitionSolution

#![allow(unused)]
fn main() {
pub struct PartitionSolution<T, B> {
    pub coefficient: usize,
    pub bins: Vec<(B, Vec<T>)>,
}
}

Function: A single partition solution.

FieldTypeDescription
coefficientusizeThe multinomial coefficient
binsVec<(B, Vec<T>)>The name and contents of each bin

E-Graph Simplification

Feature Gate: requires the egg feature. Add to Cargo.toml:

[dependencies]
ocas-rewrite = { version = "0.23", features = ["egg"] }

simplify_with_egraph

#![allow(unused)]
fn main() {
pub fn simplify_with_egraph<'a>(
    atom: Atom<'a>,
    ocas_arena: &'a AtomArena<'a>,
    iter_limit: usize,
) -> Atom<'a>
}

Function: Simplifies an expression using the egg equality saturation engine. Converts the oCAS expression into an egg E-graph, runs the built-in rule set, and extracts the optimal expression using the AST size as the cost function.

Parameters:

ParameterTypeDescription
atomAtom<'a>The expression to simplify
ocas_arena&AtomArena<'a>The oCAS expression arena
iter_limitusizeThe equality saturation iteration limit

Return value: Atom<'a> — the simplified expression.

Built-in E-graph rules:

Rule nameRewrite
add-zero(add 0 ?a) → ?a
mul-zero(mul ?a 0) → 0
mul-one(mul 1 ?a) → ?a
pow-zero(pow ?a 0) → 1
pow-one(pow ?a 1) → ?a
pythagorean$\sin^2 x + \cos^2 x \to 1$

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use ocas_rewrite::egraph::simplify_with_egraph;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

let x = ctx.var("x");
let two = ctx.num(2);
let sin_x = ctx.fun("sin", &[x]);
let cos_x = ctx.fun("cos", &[x]);
let sum = ctx.add(&[ctx.pow(sin_x, two), ctx.pow(cos_x, two)]);

let result = simplify_with_egraph(sum, &ctx, 5);
assert_eq!(result.to_string(), "1");
}

See also: AtomLanguage (ocas-rewrite::egraph)


AtomLanguage

#![allow(unused)]
fn main() {
pub enum AtomLanguage {
    Num(i64),
    Var(Symbol),
    Fun(Vec<Id>),
    Add(Vec<Id>),
    Mul(Vec<Id>),
    Pow([Id; 2]),
}
}

Function: The oCAS implementation of the egg::Language trait, used for E-graph node representation.

Note: The Language trait is implemented manually because egg's define_language! macro does not support i64 and Symbol as leaf types.

AtomLanguage::to_recexpr

#![allow(unused)]
fn main() {
pub fn to_recexpr<'b>(
    atom: Atom<'b>,
    egraph: &mut egg::EGraph<Self, ()>,
    cache: &mut Vec<(Atom<'b>, Id)>,
) -> Id
}

Function: Converts an oCAS Atom into an egg RecExpr. Shared subexpressions are deduplicated via cache.

AtomLanguage::from_recexpr

#![allow(unused)]
fn main() {
pub fn from_recexpr<'a>(
    expr: &RecExpr<Self>,
    id: Id,
    ocas_arena: &'a AtomArena<'a>,
) -> Atom<'a>
}

Function: Converts an egg RecExpr back into an oCAS Atom.

See also: simplify_with_egraph, egg::Language


Module Dependencies

graph TD
    A[ocas-atom] --> B[ocas-rewrite]
    C[ocas-core] --> B
    B --> D[pattern]
    B --> E[matcher]
    B --> F[rules]
    B --> G[simplify]
    B --> H[replace]
    B --> I[transformer]
    B --> J[combinatorics]
    B --> K[egraph]
    D --> E
    D --> F
    E --> F
    E --> G
    E --> H
    F --> G
    I --> G
    J --> I
    K -.->|feature = "egg"| B

Rust API Reference: Evaluation and JIT

oCAS's evaluation system compiles symbolic expressions into stack-machine instruction sequences and then executes them efficiently on numeric types. Three execution backends are supported: an interpreter (general purpose), JIT compilation (Cranelift generates native machine code), and SIMD batch evaluation (vectorized processing of many parameter sets).

Module path: ocas_eval

Compilation pipeline:

Atom (arena-backed)
  → EvalTree (owned intermediate, optional constant-folding optimization)
  → Instr sequence (compilation + optimization passes)
  → ExpressionEvaluator (stack VM interpreter)
  → or JitCompiledFunction (Cranelift, feature = "jit")
  → or VectorEvaluator (SIMD batch, feature = "simd")

Feature flags:

FlagPurpose
jitenables the Cranelift JIT compilation backend
simdenables SIMD batch evaluation (pulp, runtime detection of SSE2/AVX2/AVX-512)
fast-polyenables fast polynomial evaluation optimizations

Imports:

#![allow(unused)]
fn main() {
use ocas_eval::{
    ExpressionEvaluator, EvaluationDomain, PowfExtension,
    FunctionMap, EvalTree,
    Instr, Instruction, Slot,
    EvaluationError,
    StreamingEvaluator,
    compile_atom, compile_atom_with,
    compile_atoms_multi, compile_atoms_multi_with,
};
use ocas_eval::instruction::BuiltinOp;

// JIT (feature = "jit")
use ocas_eval::jit::{JitCompiledFunction, JitCompiledF32, JitEngine, FloatWidth};

// SIMD (feature = "simd")
use ocas_eval::simd::{VectorEvaluator, VectorEvaluatorF32};
}

EvalTree

Signature:

#![allow(unused)]
fn main() {
pub enum EvalTree {
    Num(f64),
    Var(String),
    Fun(String, Vec<EvalTree>),
    Add(Vec<EvalTree>),
    Mul(Vec<EvalTree>),
    Pow(Box<EvalTree>, Box<EvalTree>),
}
}

Description: an arena-independent, owned intermediate representation. Converted from Atom, it is decoupled from the arena lifetime, so the compilation pipeline can run multiple optimization passes freely.

Variants:

VariantDescription
Num(f64)numeric constant
Var(String)variable reference
Fun(String, Vec<EvalTree>)named function application
Add(Vec<EvalTree>)summation
Mul(Vec<EvalTree>)product
Pow(Box<EvalTree>, Box<EvalTree>)power base^exp

EvalTree::from_atom

Signature: pub fn from_atom(atom: Atom<'_>) -> Self

Description: converts an Atom into an owned EvalTree. Recursively traverses the expression tree, turning arena handles into independent data.

Parameters:

ParameterTypeDescription
atomAtom<'_>the source expression

Returns: an owned EvalTree.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::AtomArena;
use ocas_eval::EvalTree;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let atom = ctx.add(&[ctx.var("x"), ctx.num(2)]);
let tree = EvalTree::from_atom(atom);
// tree = Add([Var("x"), Num(2.0)])
}

EvalTree::fold_constants

Signature: pub fn fold_constants(&self) -> EvalTree

Description: folds constant subtrees and applies algebraic identities. Runs before compilation to reduce the instruction count.

Optimization rules:

PatternRule
Addremove zero terms; sum fully constant terms; fold single terms
Mulabsorb zero factors; remove unit factors; multiply fully constant factors; fold single factors
Powx^1 → x, x^0 → 1, evaluate Num^Num directly
Funevaluate built-in functions with fully constant arguments directly (external functions are not folded; they may have side effects)

Returns: an optimized new EvalTree.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::EvalTree;

let tree = EvalTree::Add(vec![
    EvalTree::Num(3.0),
    EvalTree::Mul(vec![
        EvalTree::Num(0.0),
        EvalTree::Var("x".into()),
    ]),
]);
let folded = tree.fold_constants();
assert_eq!(folded, EvalTree::Num(3.0));
// Output: 3 + 0*x → 3
}

ExpressionEvaluator<T>

Signature:

#![allow(unused)]
fn main() {
pub struct ExpressionEvaluator<T: EvaluationDomain> { /* private fields */ }
}

Description: a compiled expression that can be evaluated numerically many times. It holds the instruction sequence, a preallocated stack, the constant table, and an optional user-defined function registry. The type parameter T implements EvaluationDomain, usually f64.

Compilation methods

ExpressionEvaluator::compile

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> ExpressionEvaluator<T> {
    pub fn compile(atom: Atom<'_>) -> Result<Self>
}
}

Description: compiles an Atom into an executable evaluator. Internally it first converts to an EvalTree, then folds constants and generates the instruction sequence.

Parameters:

ParameterTypeDescription
atomAtom<'_>the source expression

Returns: Ok(ExpressionEvaluator<T>) or a compilation error.

Errors: see EvaluationError.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.mul(&[ctx.var("x"), ctx.var("x")]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
assert_eq!(eval.param_count(), 1);
assert_eq!(eval.result_count(), 1);
}

ExpressionEvaluator::compile_with

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> ExpressionEvaluator<T> {
    pub fn compile_with(atom: Atom<'_>, map: FunctionMap<T>) -> Result<Self>
}
}

Description: compiles an expression with a user-defined function registry. ExternalFun nodes appearing in the expression are resolved through map.

Parameters:

ParameterTypeDescription
atomAtom<'_>the source expression
mapFunctionMap<T>the user-defined function registry (pass an empty map when no external functions are needed)

Returns: Ok(ExpressionEvaluator<T>) or a compilation/resolution error.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, FunctionMap};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.fun("myfunc", &[ctx.var("x")]);

let mut fmap = FunctionMap::<f64>::new();
fmap.register("myfunc", 1, Box::new(|args| args[0] * args[0]));

let eval = ExpressionEvaluator::compile_with(expr, fmap).unwrap();
let result = eval.evaluate(&[3.0]).unwrap();
assert_eq!(result, vec![9.0]);
// Output: myfunc(3) = 3² = 9
}

ExpressionEvaluator::compile_multi

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> ExpressionEvaluator<T> {
    pub fn compile_multi(atoms: &[Atom<'_>]) -> Result<Self>
}
}

Description: compiles multiple Atoms into a single multi-output evaluator. Constants, common subexpressions (CSE), and stack slots are shared across all outputs to avoid redundant computation.

Parameters:

ParameterTypeDescription
atoms&[Atom<'_>]the multiple expressions

Returns: Ok(ExpressionEvaluator<T>) with result_count() == atoms.len().

Example:

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr1 = ctx.mul(&[x, x]);       // x²
let expr2 = ctx.add(&[x, ctx.num(1)]); // x+1

let eval: ExpressionEvaluator<f64> =
    ExpressionEvaluator::compile_multi(&[expr1, expr2]).unwrap();
let results = eval.evaluate(&[3.0]).unwrap();
assert_eq!(results, vec![9.0, 4.0]);
// Output: x²=9, x+1=4
}

ExpressionEvaluator::compile_multi_with

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> ExpressionEvaluator<T> {
    pub fn compile_multi_with(atoms: &[Atom<'_>], map: FunctionMap<T>) -> Result<Self>
}
}

Description: the compile_multi variant with a function map (the map is passed by value, not as an Option).

Execution methods

ExpressionEvaluator::evaluate

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> ExpressionEvaluator<T> {
    pub fn evaluate(&self, params: &[T]) -> Result<Vec<T>>
}
}

Description: executes the compiled expression with the given parameter values. Allocates a fresh stack and result buffer on every call.

Parameters:

ParameterTypeDescription
params&[T]the parameter values; length must equal param_count()

Returns: Ok(Vec<T>) with length equal to result_count().

Errors:

ErrorCondition
WrongArityparams.len() != param_count()
DivisionByZerodivision by zero encountered during evaluation
FunctionNotFoundan external function is not registered

Example:

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.mul(&[ctx.var("x"), ctx.var("x")]), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let result = eval.evaluate(&[3.0]).unwrap();
assert_eq!(result[0], 10.0);
// Output: 3² + 1 = 10
}

ExpressionEvaluator::evaluate_with_stack

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> ExpressionEvaluator<T> {
    pub fn evaluate_with_stack(
        &self,
        params: &[T],
        stack: &mut Vec<T>,
        results: &mut Vec<T>,
    ) -> Result<()>
}
}

Description: evaluates using caller-provided buffers, avoiding per-call heap allocation. Prefer this method in streaming and batch scenarios.

Parameters:

ParameterTypeDescription
params&[T]the parameter values
stack&mut Vec<T>the stack buffer (automatically resized to stack_size())
results&mut Vec<T>the result buffer (filled automatically)

Returns: Ok(()) or an evaluation error.

Accessors

ExpressionEvaluator::param_count

Signature: pub fn param_count(&self) -> usize

Description: returns the expected number of parameters.

ExpressionEvaluator::result_count

Signature: pub fn result_count(&self) -> usize

Description: returns the number of outputs (1 for a single-output expression, possibly greater for one created by compile_multi).

ExpressionEvaluator::stack_size

Signature: pub fn stack_size(&self) -> usize

Description: returns the stack size needed for evaluation (parameters + constants + temporaries + outputs). The stack argument of evaluate_with_stack should be preallocated with this capacity.


JIT compilation (feature = "jit")

The JIT backend uses Cranelift to compile the instruction sequence into native machine code. Constants are embedded as immediates, and libm is used for transcendental functions. Performance is typically more than 10× faster than the interpreter.

ExpressionEvaluator::compile_jit

Signature:

#![allow(unused)]
fn main() {
#[cfg(feature = "jit")]
impl ExpressionEvaluator<f64> {
    pub fn compile_jit(&self) -> Result<JitCompiledFunction>
}
}

Description: compiles the current evaluator's instruction sequence into double-precision (f64) native code.

Returns: Ok(JitCompiledFunction) or a compilation error.

Errors:

ErrorCondition
UnsupportedOperationcontains instructions the JIT cannot lower (e.g., external functions, sec/csc/cot)
JitCompilationErrorthe Cranelift backend failed to compile

Example:

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.mul(&[ctx.var("x"), ctx.var("x")]), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let jit = eval.compile_jit().unwrap();
let result = jit.call(&[3.0]);
assert_eq!(result, vec![10.0]);
// Output: 3² + 1 = 10 (executed as native machine code)
}

ExpressionEvaluator::compile_jit_f32

Signature:

#![allow(unused)]
fn main() {
#[cfg(feature = "jit")]
impl ExpressionEvaluator<f64> {
    pub fn compile_jit_f32(&self) -> Result<JitCompiledF32>
}
}

Description: compiles the instruction sequence into single-precision (f32) native code. Constants are narrowed from f64 to f32; results have f32 precision.

Errors: same as compile_jit.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.mul(&[ctx.var("x"), ctx.num(2)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let jit_f32 = eval.compile_jit_f32().unwrap();
let result = jit_f32.call(&[3.0_f32]);
assert_eq!(result, vec![6.0_f32]);
// Output: 3 × 2 = 6 (f32 precision)
}

JitCompiledFunction

Signature:

#![allow(unused)]
fn main() {
pub struct JitCompiledFunction { /* private fields */ }
}

Description: a JIT-compiled double-precision function that can be called repeatedly.

JitCompiledFunction::call

Signature: pub fn call(&self, params: &[f64]) -> Vec<f64>

Description: calls the JIT-compiled function with the given parameters. Allocates a new result vector on every call.

Parameters:

ParameterTypeDescription
params&[f64]the parameter values

Returns: a result vector of length equal to result_count().

Example:

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.mul(&[ctx.var("x"), ctx.var("x")]), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let jit = eval.compile_jit().unwrap();
let result = jit.call(&[3.0]);
assert_eq!(result, vec![10.0]);
// Output: value of x²+1 at x=3 → 10
}

JitCompiledFunction::call_into

Signature: pub fn call_into(&self, params: &[f64], results: &mut [f64])

Description: calls the JIT function, writing results into a caller-provided buffer to avoid per-call heap allocation. results.len() must be at least result_count().

Parameters:

ParameterTypeDescription
params&[f64]the parameter values
results&mut [f64]the result buffer

Example:

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.mul(&[ctx.var("x"), ctx.var("x")]), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let jit = eval.compile_jit().unwrap();
let mut buf = vec![0.0_f64; jit.result_count()];
jit.call_into(&[42.0], &mut buf);
assert_eq!(buf, vec![1765.0]);
// Results are in buf (42² + 1 = 1765), no heap allocation
}

JitCompiledFunction::param_count

Signature: pub fn param_count(&self) -> usize

Description: returns the expected number of parameters.

JitCompiledFunction::result_count

Signature: pub fn result_count(&self) -> usize

Description: returns the number of results.

JitCompiledF32

Signature:

#![allow(unused)]
fn main() {
pub struct JitCompiledF32 { /* private fields */ }
}

Description: a JIT-compiled single-precision function. The API is fully symmetric with JitCompiledFunction; parameters and return values are f32.

JitCompiledF32::call

Signature: pub fn call(&self, params: &[f32]) -> Vec<f32>

JitCompiledF32::call_into

Signature: pub fn call_into(&self, params: &[f32], results: &mut [f32])

JitCompiledF32::param_count

Signature: pub fn param_count(&self) -> usize

JitCompiledF32::result_count

Signature: pub fn result_count(&self) -> usize

JitEngine

Signature:

#![allow(unused)]
fn main() {
pub struct JitEngine;
}

Description: the entry point of the JIT compilation engine. Provides the low-level compile method that compiles directly from an instruction sequence.

JitEngine::compile

Signature:

#![allow(unused)]
fn main() {
impl JitEngine {
    pub fn compile(
        instructions: &[Instr],
        param_count: usize,
        constants: &[f64],
        result_indices: &[usize],
    ) -> Result<JitCompiledFunction>

    pub fn compile_f32(
        instructions: &[Instr],
        param_count: usize,
        constants: &[f32],
        result_indices: &[usize],
    ) -> Result<JitCompiledF32>
}
}

Description: compiles an instruction sequence into a callable JIT function. compile produces a double-precision JitCompiledFunction; compile_f32 produces a single-precision JitCompiledF32 (libm calls use the *f symbol variants). This is the low-level implementation behind ExpressionEvaluator::compile_jit / compile_jit_f32. Internally, compile_module returns the JIT module (kept alive by the caller) and the entry function pointer, but only the compiled function objects are exposed publicly.

Parameters:

ParameterTypeDescription
instructions&[Instr]the instruction sequence
param_countusizethe number of parameters
constants&[f64] (compile) / &[f32] (compile_f32)the constant values (embedded as immediates)
result_indices&[usize]the result slot indices

FloatWidth

Signature:

#![allow(unused)]
fn main() {
pub enum FloatWidth {
    F32,
    F64,
}
}

Description: the floating-point width selection for JIT code generation. F32 uses the *f variants of libm functions (e.g., sinf); F64 uses standard libm.


SIMD batch evaluation (feature = "simd")

VectorEvaluator uses the pulp crate to provide runtime-detected SIMD-vectorized evaluation. On x86_64 it automatically selects SSE2 (2-lane f64), AVX2 (4-lane f64), or AVX-512 (8-lane f64). The f32 variant has twice the lane count.

VectorEvaluator

Signature:

#![allow(unused)]
fn main() {
pub struct VectorEvaluator { /* private fields */ }
}

Description: a double-precision SIMD batch evaluator. Splits multiple sets of input parameters into SIMD-width chunks processed in parallel; the remainder falls back to scalar.

ExpressionEvaluator::compile_vector_evaluator

Signature:

#![allow(unused)]
fn main() {
#[cfg(feature = "simd")]
impl ExpressionEvaluator<f64> {
    pub fn compile_vector_evaluator(&self) -> Result<VectorEvaluator>
}
}

Description: creates a SIMD batch evaluator from a compiled ExpressionEvaluator. Detects the SIMD width at construction and preallocates the stack and constant buffers. Returns UnsupportedOperation if the expression contains external functions (not supported in SIMD mode).

Parameters:

ParameterTypeDescription
self&ExpressionEvaluator<f64>the source evaluator

Example:

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, simd::VectorEvaluator};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.mul(&[ctx.var("x"), ctx.var("x")]), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let simd_eval = eval.compile_vector_evaluator().unwrap();
}

VectorEvaluator::evaluate

Signature:

#![allow(unused)]
fn main() {
impl VectorEvaluator {
    pub fn evaluate(&self, params: &[Vec<f64>]) -> Result<Vec<Vec<f64>>>
}
}

Description: batch evaluation. params[i] holds all sample points of the i-th parameter. All params[i] must have the same length. Returns Vec<Vec<f64>> where result[j][i] is the j-th output at the i-th sample point.

Parameters:

ParameterTypeDescription
params&[Vec<f64>]the parameter vectors, each Vec of the same length

Returns: Ok(Vec<Vec<f64>>) or an evaluation error.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, simd::VectorEvaluator};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.mul(&[ctx.var("x"), ctx.var("x")]), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let simd_eval = eval.compile_vector_evaluator().unwrap();
let x_values: Vec<f64> = (0..1000).map(|i| i as f64 * 0.01).collect();
let results = simd_eval.evaluate(&[x_values]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].len(), 1000);
assert_eq!(results[0][0], 1.0);
// results[0] holds the 1000 values of x²+1, computed in parallel via SIMD
}

VectorEvaluatorF32

Signature:

#![allow(unused)]
fn main() {
pub struct VectorEvaluatorF32 { /* private fields */ }
}

Description: a single-precision SIMD batch evaluator. Its lane count is twice that of VectorEvaluator (e.g., 8-lane f32 vs 4-lane f64 on AVX2).

ExpressionEvaluator::compile_vector_evaluator_f32

Signature:

#![allow(unused)]
fn main() {
#[cfg(feature = "simd")]
impl ExpressionEvaluator<f64> {
    pub fn compile_vector_evaluator_f32(&self) -> Result<VectorEvaluatorF32>
}
}

Description: creates an f32 SIMD evaluator from an f64 evaluator, narrowing the constants to f32. External functions are likewise unsupported (returns UnsupportedOperation).

VectorEvaluatorF32::evaluate

Signature: pub fn evaluate(&self, params: &[Vec<f32>]) -> Result<Vec<Vec<f32>>>

Description: single-precision batch evaluation; the interface is symmetric with VectorEvaluator::evaluate.


StreamingEvaluator<'a, T>

Signature:

#![allow(unused)]
fn main() {
pub struct StreamingEvaluator<'a, T: EvaluationDomain> {
    evaluator: &'a ExpressionEvaluator<T>,
    params: Vec<T>,
    stack: Vec<T>,
    results: Vec<T>,
}
}

Description: a streaming evaluator that reuses internal buffers, keeping memory usage constant for input streams of arbitrary length. All scratch memory (parameter staging, evaluation stack, result buffers) is allocated once at construction and reused for every row.

StreamingEvaluator::new

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> StreamingEvaluator<'_, T> {
    pub fn new(evaluator: &ExpressionEvaluator<T>) -> StreamingEvaluator<'_, T>
}
}

Description: creates a streaming evaluator, preallocating all buffers.

Parameters:

ParameterTypeDescription
evaluator&ExpressionEvaluator<T>the source evaluator (borrowed)

Example:

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, StreamingEvaluator};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.var("x"), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let mut stream = StreamingEvaluator::new(&eval);
}

StreamingEvaluator::for_each

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> StreamingEvaluator<'_, T> {
    pub fn for_each<I, S, F>(&mut self, rows: I, mut sink: F) -> Result<usize>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<[T]>,
        F: FnMut(&[T]),
}
}

Description: processes every row of the input stream, calling the sink callback with each row's results. Memory usage is constant and does not grow with the number of rows.

Parameters:

ParameterTypeDescription
rowsI: IntoIterator<Item = S>the input row iterator; each row is AsRef<[T]> of length param_count()
sinkFnMut(&[T])the result callback; its &[T] has length result_count() and is only valid inside the callback

Returns: Ok(number of rows processed) or an evaluation error.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, StreamingEvaluator};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.var("x"), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let mut stream = StreamingEvaluator::new(&eval);
let rows = (0..1_000_000).map(|i| [i as f64]);
let n = stream.for_each(rows, |results| {
    // results[0] is x+1 for the current row, only valid inside the callback
}).unwrap();
assert_eq!(n, 1_000_000);
}

See also: StreamingEvaluator::evaluate_chunk

StreamingEvaluator::evaluate_chunk

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain + PowfExtension> StreamingEvaluator<'_, T> {
    pub fn evaluate_chunk<S: AsRef<[T]>>(&mut self, rows: &[S]) -> Result<Vec<Vec<T>>>
}
}

Description: processes a batch of rows and collects all results. Suitable for bounded batches; for unbounded streams use for_each.

Parameters:

ParameterTypeDescription
rows&[S]the input row slice

Returns: Ok(Vec<Vec<T>>), each Vec<T> holding one row's results.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, StreamingEvaluator};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.var("x"), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let mut stream = StreamingEvaluator::new(&eval);
let rows: Vec<[f64; 1]> = (0..100).map(|i| [i as f64]).collect();
let all_results = stream.evaluate_chunk(&rows).unwrap();
assert_eq!(all_results.len(), 100);
assert_eq!(all_results[0][0], 1.0); // x=0 → 0+1=1
assert_eq!(all_results[99][0], 100.0); // x=99 → 99+1=100
}

FunctionMap<T>

Signature:

#![allow(unused)]
fn main() {
pub struct FunctionMap<T: EvaluationDomain> {
    entries: Vec<(String, FunctionEntry<T>)>,
    name_to_idx: HashMap<String, usize>,
    aliases: HashMap<String, String>,
}
}

Description: a user-defined function registry. Allows calling registered external functions during expression evaluation. Name lookup is case-insensitive and supports aliases.

FunctionMap::new

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain> FunctionMap<T> {
    pub fn new() -> Self
}
}

Description: creates an empty function map.

FunctionMap::register

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain> FunctionMap<T> {
    pub fn register(&mut self, name: &str, arity: usize, func: Box<dyn Fn(&[T]) -> T + Send + Sync>)
}
}

Description: registers an external function.

Parameters:

ParameterTypeDescription
name&strthe function name (case-insensitive)
arityusizethe expected number of arguments
funcBox<dyn Fn(&[T]) -> T + Send + Sync>the function implementation

Example:

#![allow(unused)]
fn main() {
use ocas_eval::FunctionMap;

let mut fmap = FunctionMap::<f64>::new();
fmap.register("square", 1, Box::new(|args| args[0] * args[0]));
fmap.register("add3", 3, Box::new(|args| args[0] + args[1] + args[2]));
}

FunctionMap::register_alias

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain> FunctionMap<T> {
    pub fn register_alias(&mut self, alias: &str, canonical: &str)
}
}

Description: adds an alias for a registered function name. Looking up alias is redirected to canonical.

FunctionMap::resolve

Signature:

#![allow(unused)]
fn main() {
impl<T: EvaluationDomain> FunctionMap<T> {
    pub fn resolve(&self, name: &str) -> Option<&FunctionEntry<T>>
}
}

Description: looks up a function by name (resolving aliases and case).

FunctionMap::index_of

Signature: pub fn index_of(&self, name: &str) -> Option<usize>

Description: returns the function's index in the map (used for fn_idx in instructions).

FunctionMap::call_by_index

Signature: pub fn call_by_index(&self, idx: usize, args: &[T]) -> Option<T>

Description: calls a function by index.

FunctionMap::len / is_empty

Signature: pub fn len(&self) -> usize / pub fn is_empty(&self) -> bool

Description: returns the number of registered functions / whether the map is empty.

FunctionEntry<T>

Signature:

#![allow(unused)]
fn main() {
pub struct FunctionEntry<T: EvaluationDomain> {
    pub arity: usize,
    // private func field
}
}

Description: a registered external function entry. arity is the expected number of arguments.


Instruction set

Slot

Signature:

#![allow(unused)]
fn main() {
pub enum Slot {
    Param(usize),
    Const(usize),
    Temp(usize),
}
}

Description: a named slot in the evaluator stack. Used by the public Instruction type to reference stack positions semantically rather than by raw index.

VariantDescription
Param(usize)parameter slot, 0-based index
Const(usize)constant slot, 0-based index
Temp(usize)temporary value slot, 0-based index

Instruction

Signature:

#![allow(unused)]
fn main() {
pub enum Instruction {
    Add(Slot, Vec<Slot>),
    Mul(Slot, Vec<Slot>),
    Pow(Slot, Slot, i64),
    Powf(Slot, Slot, Slot),
    Fun(Slot, Symbol, Slot),
    ExternalFun(Slot, usize, Vec<Slot>),
    Assign(Slot, Slot),
}
}

Description: a self-documenting public instruction. Unlike the internal Instr, it uses Slot instead of raw indices, making it convenient for inspection and serialization.

VariantFormatDescription
Adddst = sum(sources)summation
Muldst = product(sources)product
Powdst = base^expinteger power
Powfdst = base^expfloating-point power
Fundst = builtin(src)built-in function
ExternalFundst = fns[idx](srcs...)external function
Assigndst = srccopy

Instr

Signature:

#![allow(unused)]
fn main() {
pub enum Instr {
    Add { dst: usize, srcs: Vec<usize> },
    Mul { dst: usize, srcs: Vec<usize> },
    Pow { dst: usize, base: usize, exp: i64 },
    Powf { dst: usize, base: usize, exp: usize },
    BuiltinOp { dst: usize, op: BuiltinOp, src: usize },
    ExternalFun { dst: usize, fn_idx: usize, srcs: Vec<usize> },
    Copy { dst: usize, src: usize },
}
}

Description: an internal index-based instruction, executed by ExpressionEvaluator. All indices are absolute positions in the stack.

Stack layout:

[params (param_count)] [constants (const_count)] [temporaries (temp_count)] [outputs (result_count)]
VariantDescription
Add { dst, srcs }stack[dst] = stack[srcs[0]] + stack[srcs[1]] + ...
Mul { dst, srcs }stack[dst] = stack[srcs[0]] * stack[srcs[1]] * ...
Pow { dst, base, exp }stack[dst] = stack[base]^exp, with exp an i64
Powf { dst, base, exp }stack[dst] = stack[base]^stack[exp], floating-point exponent
BuiltinOp { dst, op, src }stack[dst] = op(stack[src])
ExternalFun { dst, fn_idx, srcs }stack[dst] = fns[fn_idx](&stack[srcs...])
Copy { dst, src }stack[dst] = stack[src]

BuiltinOp

Signature:

#![allow(unused)]
fn main() {
pub enum BuiltinOp {
    Sin, Cos, Tan,
    Sec, Csc, Cot,
    Exp, Log,
    Sqrt, Abs,
}
}

Description: pre-resolved built-in function opcodes. The compiler converts Symbol names into BuiltinOp variants at compile time, avoiding string matching on the hot path.

VariantMathDescription
Sin$\sin(x)$sine
Cos$\cos(x)$cosine
Tan$\tan(x)$tangent
Sec$\sec(x) = 1/\cos(x)$secant
Csc$\csc(x) = 1/\sin(x)$cosecant
Cot$\cot(x) = 1/\tan(x)$cotangent
Exp$e^x$exponential function
Log$\ln(x)$natural logarithm
Sqrt$\sqrt{x}$square root
Abs$x

BuiltinOp::from_name

Signature: pub fn from_name(name: &str) -> Option<Self>

Description: tries to resolve a function name into a built-in opcode. Case-insensitive. Both "log" and "ln" map to Log.

Parameters:

ParameterTypeDescription
name&strthe function name

Returns: Some(BuiltinOp) or None (not a built-in function).

Example:

#![allow(unused)]
fn main() {
use ocas_eval::instruction::BuiltinOp;

assert_eq!(BuiltinOp::from_name("sin"), Some(BuiltinOp::Sin));
assert_eq!(BuiltinOp::from_name("Log"), Some(BuiltinOp::Log));
assert_eq!(BuiltinOp::from_name("ln"), Some(BuiltinOp::Log));
assert_eq!(BuiltinOp::from_name("myfunc"), None);
}

EvaluationDomain trait

Signature:

#![allow(unused)]
fn main() {
pub trait EvaluationDomain: Sized + Clone + 'static {
    fn from_f64(value: f64) -> Self;
    fn zero() -> Self;
    fn one() -> Self;
    fn add_ref(&self, other: &Self) -> Self;
    fn sub_ref(&self, other: &Self) -> Self;
    fn mul_ref(&self, other: &Self) -> Self;
    fn div_ref(&self, other: &Self) -> Result<Self>;
    fn neg_ref(&self) -> Self;
    fn powi_ref(&self, exp: i64) -> Self;
    fn resolve_builtin(name: &str, arg: &Self) -> Result<Self>;
}
}

Description: the trait for types that can serve as evaluation domains. Unlike the algebraic Domain trait (Domain is object-safe and uses &self receivers), EvaluationDomain uses static methods and value operations, making it compatible with Copy types such as f64.

Methods:

MethodSignatureDescription
from_f64fn from_f64(f64) -> Selfcreate a value from an f64
zerofn zero() -> Selfadditive identity
onefn one() -> Selfmultiplicative identity
add_reffn add_ref(&self, &Self) -> Selfaddition
sub_reffn sub_ref(&self, &Self) -> Selfsubtraction
mul_reffn mul_ref(&self, &Self) -> Selfmultiplication
div_reffn div_ref(&self, &Self) -> Result<Self>division; division by zero returns DivisionByZero
neg_reffn neg_ref(&self) -> Selfnegation
powi_reffn powi_ref(&self, i64) -> Selfinteger power; returns 1 when exp == 0
resolve_builtinfn resolve_builtin(name: &str, &Self) -> Result<Self>execute a built-in function

Implementations:

TypeDescription
f64standard double precision (always available)
DoubleF64oCAS double-precision float (~31 significant digits)

Example:

#![allow(unused)]
fn main() {
use ocas_eval::EvaluationDomain;

let x = f64::from_f64(3.0);
let y = f64::from_f64(2.0);
assert_eq!(x.add_ref(&y), 5.0);
assert_eq!(f64::resolve_builtin("sin", &std::f64::consts::FRAC_PI_2).unwrap(), 1.0);
}

PowfExtension trait

Signature:

#![allow(unused)]
fn main() {
pub trait PowfExtension: EvaluationDomain {
    fn powf_ref(&self, exp: &Self) -> Result<Self>;
}
}

Description: an extension trait for powers with floating-point exponents. It is separated from EvaluationDomain because integer domains cannot meaningfully compute $a^b$ (with non-integer $b$).

Implementations: f64, DoubleF64.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::PowfExtension;

let base = 2.0_f64;
let exp = 0.5_f64;
let result = base.powf_ref(&exp).unwrap();
assert!((result - std::f64::consts::SQRT_2).abs() < 1e-15);
// Output: 2^0.5 ≈ 1.4142135623730951
}

Compilation functions

The following free functions are the low-level entry points behind ExpressionEvaluator's compilation methods.

compile_atom

Signature:

#![allow(unused)]
fn main() {
pub fn compile_atom<T: EvaluationDomain + PowfExtension>(
    atom: Atom<'_>,
) -> Result<ExpressionEvaluator<T>>
}

Description: compiles an Atom into an evaluator. Equivalent to ExpressionEvaluator::compile.

compile_atom_with

Signature:

#![allow(unused)]
fn main() {
pub fn compile_atom_with<T: EvaluationDomain + PowfExtension>(
    atom: Atom<'_>,
    function_map: Option<FunctionMap<T>>,
) -> Result<ExpressionEvaluator<T>>
}

Description: compiles with an optional function map.

compile_tree / compile_tree_with

Signature:

#![allow(unused)]
fn main() {
pub fn compile_tree<T: EvaluationDomain + PowfExtension>(
    tree: &EvalTree,
) -> Result<ExpressionEvaluator<T>>

pub fn compile_tree_with<T: EvaluationDomain + PowfExtension>(
    tree: &EvalTree,
    function_map: Option<FunctionMap<T>>,
) -> Result<ExpressionEvaluator<T>>
}

Description: compiles from an EvalTree. Suitable when you need to apply custom optimizations to the EvalTree before compiling.

compile_atoms_multi / compile_atoms_multi_with

Signature:

#![allow(unused)]
fn main() {
pub fn compile_atoms_multi<T: EvaluationDomain + PowfExtension>(
    atoms: &[Atom<'_>],
) -> Result<ExpressionEvaluator<T>>

pub fn compile_atoms_multi_with<T: EvaluationDomain + PowfExtension>(
    atoms: &[Atom<'_>],
    function_map: Option<FunctionMap<T>>,
) -> Result<ExpressionEvaluator<T>>
}

Description: compiles multiple Atoms into a single multi-output evaluator, sharing CSEs and constants across outputs.

compile_trees_multi

Signature:

#![allow(unused)]
fn main() {
pub fn compile_trees_multi<T: EvaluationDomain + PowfExtension>(
    trees: &[&EvalTree],
) -> Result<ExpressionEvaluator<T>>
}

Description: compiles multiple EvalTrees into a single multi-output evaluator.


EvaluationError

Signature:

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum EvaluationError {
    UndefinedVariable { name: String },
    TypeMismatch { expected: String, found: String },
    DivisionByZero,
    FunctionNotFound { name: String },
    WrongArity { name: String, expected: usize, got: usize },
    JitCompilationError { message: String },
    UnsupportedOperation { message: String },
}
}

Description: the errors that can occur during evaluation.

VariantDescription
UndefinedVariable { name }the expression references a variable that was not supplied
TypeMismatch { expected, found }type mismatch (e.g., expected a float but got an integer)
DivisionByZerodivision by zero
FunctionNotFound { name }a user-defined function was not found in the registry
WrongArity { name, expected, got }wrong number of arguments in a function call
JitCompilationError { message }JIT compilation failed (Cranelift backend error)
UnsupportedOperation { message }the requested operation is not supported in the current domain

Display format:

undefined variable 'x'
type mismatch: expected float, found integer
division by zero
function 'f' not found
wrong arity for 'f': expected 2, got 1
JIT compilation error: <message>
unsupported operation: <message>

Example:

#![allow(unused)]
fn main() {
use ocas_eval::EvaluationError;

let err = EvaluationError::UndefinedVariable { name: "x".into() };
assert_eq!(err.to_string(), "undefined variable 'x'");

let err = EvaluationError::WrongArity {
    name: "f".into(),
    expected: 2,
    got: 1,
};
assert_eq!(err.to_string(), "wrong arity for 'f': expected 2, got 1");
}

Usage patterns

Basic evaluation

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// Build x² + 2x + 1
let x = ctx.var("x");
let expr = ctx.add(&[
    ctx.mul(&[x, x]),
    ctx.mul(&[ctx.num(2), x]),
    ctx.num(1),
]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();

// Evaluate
let result = eval.evaluate(&[3.0]).unwrap();
assert_eq!(result[0], 16.0); // 9 + 6 + 1 = 16
}

JIT hot loop

#![allow(unused)]
fn main() {
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.fun("sin", &[ctx.var("x")]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let jit = eval.compile_jit().unwrap();

let mut results = vec![0.0; jit.result_count()];
for i in 0..1_000_000 {
    jit.call_into(&[i as f64 * 0.001], &mut results);
    // use results[0]...
}
// Zero-allocation hot loop, native machine code
}

SIMD batch processing

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, simd::VectorEvaluator};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.fun("exp", &[ctx.var("x")]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let simd = eval.compile_vector_evaluator().unwrap();

let x: Vec<f64> = (0..10000).map(|i| i as f64 * 0.001).collect();
let results = simd.evaluate(&[x]).unwrap();
// results[0] holds the 10000 values of e^x
}

Streaming large datasets

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, StreamingEvaluator};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.add(&[ctx.var("x"), ctx.num(1)]);

let eval: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let mut stream = StreamingEvaluator::new(&eval);

// Process 1 million rows with constant memory usage
let rows = (0..1_000_000).map(|i| [i as f64]);
let n = stream.for_each(rows, |_results| {
    // process results...
}).unwrap();
assert_eq!(n, 1_000_000);
}

Custom functions

#![allow(unused)]
fn main() {
use ocas_eval::{ExpressionEvaluator, FunctionMap};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.fun("sigmoid", &[ctx.var("x")]);

let mut fmap = FunctionMap::<f64>::new();
fmap.register("sigmoid", 1, Box::new(|args| {
    1.0 / (1.0 + (-args[0]).exp())
}));

let eval = ExpressionEvaluator::compile_with(expr, fmap).unwrap();
let result = eval.evaluate(&[0.0]).unwrap();
assert!((result[0] - 0.5).abs() < 1e-15);
// Output: sigmoid(0) = 0.5
}

See also: Expression system (Atom, AtomArena), Numerical integration (Vegas uses the evaluator), Automatic differentiation (evaluation over the HyperDual domain)

Automatic Differentiation

oCAS implements forward-mode automatic differentiation via hyper-dual numbers. A HyperDual<T> carries a scalar value and a set of derivative components. The component layout is precomputed by a shared DualShape (shared through an Arc), and arithmetic operations use a prebuilt multiplication table.

Source file: ocas-domain/src/dual.rs

DualCoeff trait

#![allow(unused)]
fn main() {
pub trait DualCoeff:
    Clone
    + PartialEq
    + std::ops::Add<Output = Self>
    + std::ops::Sub<Output = Self>
    + std::ops::Mul<Output = Self>
    + std::ops::Div<Output = Self>
    + std::ops::Neg<Output = Self>
    + std::ops::AddAssign
    + std::ops::MulAssign
{
    fn zero() -> Self;
    fn one() -> Self;
}
}

Description: defines the coefficient type constraints required by hyper-dual numbers. Requires the full four arithmetic operations (including multiplicative inverses) plus additive and multiplicative identities.

Parameters: none (trait bound).

Built-in implementations:

TypeDescription
Rationalarbitrary-precision rational; zero() = 0/1, one() = 1/1

Limitation: currently only Rational implements DualCoeff. f64 and other floating-point types are not supported as coefficients.

DualShape

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct DualShape { /* private fields */ }
}

Description: describes the component layout of a hyper-dual number. Each component is identified by a multi-index — a vector of non-negative integers of length equal to the number of variables, recording the order of differentiation with respect to each variable. The layout must satisfy ancestor closure: if the multi-index $\mathbf{m}$ is present, then every multi-index that is component-wise ≤ $\mathbf{m}$ must also be present.

Component 0 is always the all-zero multi-index (the scalar value component).

DualShape::new

#![allow(unused)]
fn main() {
pub fn new(components: Vec<Vec<usize>>) -> Option<Self>
}

Description: builds a layout from an ancestor-closed list of multi-indices.

Parameters:

ParameterTypeDescription
componentsVec<Vec<usize>>the list of multi-indices; each Vec<usize> must have the same length (missing entries are zero-padded automatically)

Returns: Some(DualShape) if the layout is valid, otherwise None.

Error conditions:

  • empty list → None
  • no all-zero multi-index → None
  • ancestor closure violated → None

Example:

#![allow(unused)]
fn main() {
use ocas_domain::dual::DualShape;

// First order, two variables: value + ∂/∂x₀ + ∂/∂x₁
let shape = DualShape::new(vec![
    vec![0, 0],
    vec![1, 0],
    vec![0, 1],
]).unwrap();
assert_eq!(shape.n_components(), 3);
assert_eq!(shape.n_vars(), 2);

// Invalid: [1] missing (not ancestor-closed)
assert!(DualShape::new(vec![vec![0], vec![2]]).is_none());
}

DualShape::n_vars

#![allow(unused)]
fn main() {
pub fn n_vars(&self) -> usize
}

Description: returns the number of differentiation variables (the length of the multi-indices).

Returns: usize

DualShape::n_components

#![allow(unused)]
fn main() {
pub fn n_components(&self) -> usize
}

Description: returns the total number of components (including the scalar value component).

Returns: usize

DualShape::components

#![allow(unused)]
fn main() {
pub fn components(&self) -> &[Vec<usize>]
}

Description: returns the slice of component multi-indices. Index 0 is the scalar value component.

Returns: &[Vec<usize>]

DualShape::index_of

#![allow(unused)]
fn main() {
pub fn index_of(&self, multi_index: &[usize]) -> Option<usize>
}

Description: looks up the component index for a given multi-index.

Parameters:

ParameterTypeDescription
multi_index&[usize]the multi-index to look up

Returns: Some(index) if present, otherwise None.

DualShape::mult_table

#![allow(unused)]
fn main() {
pub fn mult_table(&self) -> &[(usize, usize, usize)]
}

Description: returns the multiplication table (for debugging/testing). Each triple (a, b, c) means that the product of component a and component b contributes to component c. Pairs involving the scalar component (index 0) are not included.

Returns: &[(usize, usize, usize)]

new_first_order

#![allow(unused)]
fn main() {
pub fn new_first_order<T: DualCoeff>(nvars: usize) -> Arc<DualShape>
}

Description: builds a first-order shape tracking the partial derivative $\frac{\partial}{\partial x_i}$ of each variable. Does not track higher-order or mixed partials.

Parameters:

ParameterTypeDescription
nvarsusizethe number of differentiation variables

Returns: Arc<DualShape> — contains $n+1$ components: the value component $[0,\dots,0]$ plus one component $[0,\dots,\underset{i}{1},\dots,0]$ per variable.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::dual::new_first_order;
use ocas_domain::Rational;

let shape = new_first_order::<Rational>(3);
assert_eq!(shape.n_components(), 4); // value + 3 partials
assert_eq!(shape.n_vars(), 3);
}

HyperDual<T>

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct HyperDual<T: DualCoeff> { /* private fields */ }
}

Description: the hyper-dual number type, carrying a scalar value and derivative components laid out according to a DualShape. All arithmetic operations propagate derivatives automatically.

Type constraint: T: DualCoeff

HyperDual::variable

#![allow(unused)]
fn main() {
pub fn variable(shape: &Arc<DualShape>, i: usize, c: T) -> Self
}

Description: constructs the $i$-th independent variable with value $c$. The derivative component $[0,\dots,\underset{i}{1},\dots,0]$ is set to 1; all others are 0.

Parameters:

ParameterTypeDescription
shape&Arc<DualShape>the component layout
iusizethe variable index (0-based), must be < n_vars() (out-of-range indices are silently ignored, no panic)
cTthe variable's value at the evaluation point

Returns: HyperDual<T>

Example:

#![allow(unused)]
fn main() {
use ocas_domain::dual::{HyperDual, new_first_order};
use ocas_domain::Rational;

let shape = new_first_order::<Rational>(2);
let x = HyperDual::variable(&shape, 0, Rational::new(3, 1));
assert_eq!(x.value(), &Rational::new(3, 1));
assert_eq!(x.deriv(0), Some(&Rational::new(1, 1)));
assert_eq!(x.deriv(1), Some(&Rational::new(0, 1)));
}

HyperDual::constant

#![allow(unused)]
fn main() {
pub fn constant(shape: &Arc<DualShape>, c: T) -> Self
}

Description: constructs a constant — value $c$, all derivative components zero.

Parameters:

ParameterTypeDescription
shape&Arc<DualShape>the component layout
cTthe constant value

Returns: HyperDual<T>

Example:

#![allow(unused)]
fn main() {
use ocas_domain::dual::{HyperDual, new_first_order};
use ocas_domain::Rational;

let shape = new_first_order::<Rational>(1);
let c = HyperDual::constant(&shape, Rational::new(7, 1));
assert_eq!(c.value(), &Rational::new(7, 1));
assert_eq!(c.deriv(0), Some(&Rational::new(0, 1)));
}

HyperDual::value

#![allow(unused)]
fn main() {
pub fn value(&self) -> &T
}

Description: returns a reference to the scalar value component (component 0).

Returns: &T

HyperDual::deriv

#![allow(unused)]
fn main() {
pub fn deriv(&self, i: usize) -> Option<&T>
}

Description: returns the first-order partial derivative $\frac{\partial f}{\partial x_i}$ with respect to variable $i$.

Parameters:

ParameterTypeDescription
iusizethe variable index (0-based)

Returns: Some(&T) if the first-order component $[0,\dots,\underset{i}{1},\dots,0]$ exists in the shape (for new_first_order shapes this is equivalent to $i < n_vars$), otherwise None.

HyperDual::values

#![allow(unused)]
fn main() {
pub fn values(&self) -> &[T]
}

Description: returns a slice of all components, in shape order.

Returns: &[T]

HyperDual::shape

#![allow(unused)]
fn main() {
pub fn shape(&self) -> &Arc<DualShape>
}

Description: returns the shared shape reference.

Returns: &Arc<DualShape>

HyperDual::from_values

#![allow(unused)]
fn main() {
pub fn from_values(shape: Arc<DualShape>, values: Vec<T>) -> Option<Self>
}

Description: constructs from a full component vector. The length must match the shape's component count.

Parameters:

ParameterTypeDescription
shapeArc<DualShape>the component layout
valuesVec<T>the component values; length must be shape.n_components()

Returns: Some(HyperDual<T>) if the length matches, otherwise None.

HyperDual::zero / HyperDual::one

#![allow(unused)]
fn main() {
pub fn zero(shape: &Arc<DualShape>) -> Self
pub fn one(shape: &Arc<DualShape>) -> Self
}

Description: construct the additive identity (all components zero) and the multiplicative identity (value 1, derivative components zero).

HyperDual::inv

#![allow(unused)]
fn main() {
pub fn inv(&self) -> Option<Self>
}

Description: the multiplicative inverse $\frac{1}{f}$, computed by truncating the geometric series $\frac{1}{v+\varepsilon} = \frac{1}{v}\sum_{k \geq 0}\left(-\frac{\varepsilon}{v}\right)^k$.

Returns: Some(1/self) if the value component is nonzero, otherwise None (division by zero).

Arithmetic operations

HyperDual<T> implements the following standard traits; all operations propagate derivatives automatically:

TraitOperationDerivative rule
Adda + b$\frac{\partial}{\partial x_i}(a+b) = a'_i + b'_i$
Suba - b$\frac{\partial}{\partial x_i}(a-b) = a'_i - b'_i$
Neg-a$\frac{\partial}{\partial x_i}(-a) = -a'_i$
Mula * b$\frac{\partial}{\partial x_i}(ab) = a'b + ab'$ (higher-order terms handled via the multiplication table)
Diva / bequivalent to a * b.inv(); requires the value component of b to be nonzero

Constraint: Add, Sub, Mul, Div require both operands to share the same DualShape. In debug mode the component counts are checked, and mismatches panic.

Division by zero: Div calls inv(), which panics if the divisor's value component is zero.

Complete examples

Example 1: partial derivatives of a two-variable function

Compute the value and partial derivatives of $f(x, y) = x \cdot y$ at $(3, 5)$:

#![allow(unused)]
fn main() {
use ocas_domain::dual::{HyperDual, new_first_order};
use ocas_domain::Rational;

let shape = new_first_order::<Rational>(2);
let x = HyperDual::variable(&shape, 0, Rational::new(3, 1));
let y = HyperDual::variable(&shape, 1, Rational::new(5, 1));

let f = x * y;

assert_eq!(f.value(), &Rational::new(15, 1));     // f(3,5) = 15
assert_eq!(f.deriv(0), Some(&Rational::new(5, 1))); // ∂f/∂x = y = 5
assert_eq!(f.deriv(1), Some(&Rational::new(3, 1))); // ∂f/∂y = x = 3
}

Example 2: derivative of a power function

Compute the derivative of $f(x) = x^3$ at $x=3$: $\frac{d}{dx}x^3 = 3x^2 = 27$:

#![allow(unused)]
fn main() {
use ocas_domain::dual::{HyperDual, new_first_order};
use ocas_domain::Rational;

let shape = new_first_order::<Rational>(1);
let x = HyperDual::variable(&shape, 0, Rational::new(3, 1));

// x^3 = x * x * x
let x2 = x.clone() * x.clone();
let x3 = x2 * x;

assert_eq!(x3.value(), &Rational::new(27, 1));
assert_eq!(x3.deriv(0), Some(&Rational::new(27, 1)));
}

Example 3: derivative of a quotient

Compute $f(x,y) = \frac{x}{y}$ at $(6, 3)$: $\frac{\partial f}{\partial x} = \frac{1}{y} = \frac{1}{3}$, $\frac{\partial f}{\partial y} = -\frac{x}{y^2} = -\frac{2}{3}$.

#![allow(unused)]
fn main() {
use ocas_domain::dual::{HyperDual, new_first_order};
use ocas_domain::Rational;

let shape = new_first_order::<Rational>(2);
let x = HyperDual::variable(&shape, 0, Rational::new(6, 1));
let y = HyperDual::variable(&shape, 1, Rational::new(3, 1));

let f = x / y;

assert_eq!(f.value(), &Rational::new(2, 1));
assert_eq!(f.deriv(0), Some(&Rational::new(1, 3)));
assert_eq!(f.deriv(1), Some(&Rational::new(-2, 3)));
}

Example 4: three-variable product

Compute $f(x,y,z) = xyz$ at $(2, 3, 5)$: $\frac{\partial f}{\partial x} = yz = 15$, $\frac{\partial f}{\partial y} = xz = 10$, $\frac{\partial f}{\partial z} = xy = 6$.

#![allow(unused)]
fn main() {
use ocas_domain::dual::{HyperDual, new_first_order};
use ocas_domain::Rational;

let shape = new_first_order::<Rational>(3);
let x = HyperDual::variable(&shape, 0, Rational::new(2, 1));
let y = HyperDual::variable(&shape, 1, Rational::new(3, 1));
let z = HyperDual::variable(&shape, 2, Rational::new(5, 1));

let f = x * y * z;

assert_eq!(f.value(), &Rational::new(30, 1));
assert_eq!(f.deriv(0), Some(&Rational::new(15, 1)));
assert_eq!(f.deriv(1), Some(&Rational::new(10, 1)));
assert_eq!(f.deriv(2), Some(&Rational::new(6, 1)));
}

Example 5: second derivative

Use a custom shape to track second derivatives. For $f(x) = x^2$, $f''(x) = 2$. Note: the component with multi-index $[2]$ stores the coefficient of $\varepsilon^2$, i.e. $f''(x)/2!$, not $f''(x)$ itself.

#![allow(unused)]
fn main() {
use ocas_domain::dual::{DualShape, HyperDual};
use ocas_domain::Rational;

// Shape: [0] (value), [1] (first order), [2] (second order)
let shape = std::sync::Arc::new(
    DualShape::new(vec![vec![0], vec![1], vec![2]]).unwrap()
);
let x = HyperDual::variable(&shape, 0, Rational::new(3, 1));

// f = x * x
let f = x.clone() * x;

assert_eq!(f.value(), &Rational::new(9, 1));     // 3^2 = 9
assert_eq!(f.deriv(0), Some(&Rational::new(6, 1))); // 2x = 6
// Component [2] stores the coefficient of ε² = f''(3)/2! = 2/2 = 1
assert_eq!(f.values()[2], Rational::new(1, 1));  // f''(3)/2! = 1
}

Implementation details

Multiplication table

DualShape precomputes a multiplication table: for every pair of non-scalar components $(a, b)$, if the sum of their multi-indices is also present in the layout, record the triple $(a, b, c)$. During multiplication, component $k$ is computed as:

$$\text{result}[k] = a[k] \cdot b[0] + a[0] \cdot b[k] + \sum_{(i,j,k) \in \text{table}} a[i] \cdot b[j]$$

Higher-order terms beyond the layout are truncated.

Geometric series for the inverse

inv() computes via the geometric series $\frac{1}{v+\varepsilon} = \frac{1}{v}\sum_{p=0}^{\infty}\left(-\frac{\varepsilon}{v}\right)^p$, iterating until the higher-order terms vanish (automatically truncated to the shape's precision).

Shared shape

All HyperDuals participating in the same expression must share the same Arc<DualShape>. Cloning the shape via Arc is very cheap.

Limitations

LimitationDescription
Rational coefficients onlyDualCoeff is currently implemented only for Rational; f64 and other floating-point types are not supported
No transcendental functionstranscendental functions such as sin, cos, exp, ln are not implemented — they require real-coefficient traits
Division-by-zero panicDiv calls inv(), which panics when the value component is zero
Shape-mismatch panicAdd/Sub/Mul/Div require the operands to share the same shape (component counts are checked in debug mode); mismatches panic
Higher-order derivatives need a manual layoutthe default new_first_order tracks only first-order partials; second and higher orders require building the layout manually via DualShape::new

See also

Rust API Reference: Tensors

oCAS provides basic tensor algebra with explicit index management. A Tensor carries named index slots (upper/lower), optional symmetry metadata, and can be converted into an Atom expression for symbolic processing. Since 0.22.0, a graph-isomorphism-based canonicalization engine and Young projectors are also available.

Module paths: ocas_atom::tensor (mod.rs), ocas_atom::tensor::spec, ocas_atom::tensor::canon, ocas_atom::tensor::dummy, ocas_atom::tensor::young

Import:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{
    IndexPosition, IndexSlot, Symmetry, Tensor,
    contract, Contracted, TensorProduct,
    symmetrise_sign,
};
use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry};
use ocas_atom::tensor::canon::{canonicalize_tensors, CanonicalTensor, TensorCanonError};
use ocas_atom::tensor::dummy::{refresh_dummies, DummyError};
use ocas_atom::tensor::young::{YoungTableau, young_project};
}

IndexPosition

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IndexPosition {
    Upper,
    Lower,
}
}

Description: The position (variance) of an index on a tensor. Upper denotes a contravariant index (superscript), Lower a covariant index (subscript). Contraction requires two indices with the same label to have opposite positions.

Variants:

VariantMeaning
UpperContravariant index (superscript); corresponds to a contravariant vector in physics
LowerCovariant index (subscript); corresponds to a covariant vector in physics

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::IndexPosition;

assert_eq!(IndexPosition::Upper, IndexPosition::Upper);
assert_ne!(IndexPosition::Upper, IndexPosition::Lower);
// Output: all assertions pass
}

IndexSlot

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct IndexSlot<'a> { /* private fields */ }
}

Description: A single index slot of a tensor, consisting of an index label (Atom) and a position (IndexPosition). It is a Copy type and can be copied cheaply.

IndexSlot::new

Signature: pub fn new(label: Atom<'a>, position: IndexPosition) -> Self

Description: Creates a new index slot.

Parameters:

ParameterTypeDescription
labelAtom<'a>Index label expression (usually a variable name such as mu or i)
positionIndexPositionIndex position: Upper or Lower

Returns: IndexSlot<'a>

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{IndexSlot, IndexPosition};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let mu = IndexSlot::new(ctx.var("mu"), IndexPosition::Upper);
let nu = IndexSlot::new(ctx.var("nu"), IndexPosition::Lower);
assert_eq!(mu.position(), IndexPosition::Upper);
assert_eq!(nu.position(), IndexPosition::Lower);
// Output: all assertions pass
}

IndexSlot::label

Signature: pub fn label(&self) -> Atom<'a>

Description: Returns the index label expression.

Returns: Atom<'a> — the Atom handle of the index label.

IndexSlot::position

Signature: pub fn position(&self) -> IndexPosition

Description: Returns the position of the index (Upper or Lower).

Returns: IndexPosition

See also: IndexPosition


Symmetry

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Symmetry {
    None,
    Symmetric,
    Antisymmetric,
}
}

Description: The symmetry of a tensor's index slots. This is advisory metadata — operations such as contract do not automatically symmetrize; the symmetry is used by downstream consumers (such as the canonicalization engine or Young projectors).

Variants:

VariantMeaning
NoneNo symmetry (generic tensor)
SymmetricSymmetric: swapping any two index slots leaves the tensor unchanged
AntisymmetricAntisymmetric: swapping any two index slots flips the sign

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::Symmetry;

assert_eq!(Symmetry::None, Symmetry::None);
assert_ne!(Symmetry::Symmetric, Symmetry::Antisymmetric);
// Output: all assertions pass
}

Tensor

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tensor<'a> { /* private fields */ }
}

Description: A named tensor object, consisting of a name (Symbol), a list of index slots (Vec<IndexSlot>), and a symmetry (Symmetry). It can be lowered to a standard Atom expression node via to_atom.

Tensor::new

Signature: pub fn new(name: Symbol, slots: Vec<IndexSlot<'a>>) -> Self

Description: Creates a new tensor; the default symmetry is Symmetry::None.

Parameters:

ParameterTypeDescription
nameSymbolTensor name (interned string, e.g. "g", "Riemann")
slotsVec<IndexSlot<'a>>List of index slots; the order is meaningful

Returns: Tensor<'a>

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let slots = vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("nu"), IndexPosition::Lower),
];
let t = Tensor::new(Symbol::new("g"), slots);
assert_eq!(t.rank(), 2);
assert_eq!(t.name().as_str(), "g");
// Output: all assertions pass
}

Tensor::with_symmetry

Signature: pub fn with_symmetry(mut self, symmetry: Symmetry) -> Self

Description: Builder method that sets the tensor's symmetry. Consumes and returns self.

Parameters:

ParameterTypeDescription
symmetrySymmetryThe desired symmetry

Returns: Self — the tensor with the symmetry set.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition, Symmetry};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let slots = vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("nu"), IndexPosition::Upper),
];
let sym = Tensor::new(Symbol::new("g"), slots.clone())
    .with_symmetry(Symmetry::Symmetric);
let anti = Tensor::new(Symbol::new("epsilon"), slots)
    .with_symmetry(Symmetry::Antisymmetric);

assert_eq!(sym.symmetry(), Symmetry::Symmetric);
assert_eq!(anti.symmetry(), Symmetry::Antisymmetric);
// Output: all assertions pass
}

Tensor::name

Signature: pub fn name(&self) -> Symbol

Description: Returns the tensor name.

Returns: Symbol — the interned string handle.

Tensor::slots

Signature: pub fn slots(&self) -> &[IndexSlot<'a>]

Description: Returns a slice reference to the index slots.

Returns: &[IndexSlot<'a>]

Tensor::symmetry

Signature: pub fn symmetry(&self) -> Symmetry

Description: Returns the tensor's symmetry.

Returns: Symmetry

Tensor::rank

Signature: pub fn rank(&self) -> usize

Description: Returns the rank of the tensor (the number of index slots).

Returns: usize

Tensor::dummy_labels

Signature: pub fn dummy_labels(&self) -> Vec<Atom<'a>>

Description: Returns the list of dummy index labels. A dummy index is a label that occurs exactly twice across all slots (once Upper and once Lower), i.e. a contraction candidate.

Returns: Vec<Atom<'a>> — the list of dummy index labels.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// A^μ_μ: mu occurs twice (Upper + Lower) → dummy index
let t = Tensor::new(Symbol::new("A"), vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("mu"), IndexPosition::Lower),
]);
assert_eq!(t.dummy_labels().len(), 1);
// Output: 1 dummy index
}

Tensor::to_atom

Signature: pub fn to_atom(&self, ctx: &'a AtomArena<'a>) -> Atom<'a>

Description: Lowers the tensor to a standard Atom function node name(slot₁, slot₂, …). No symmetrization is applied — the atom preserves self's slot order.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena

Returns: Atom<'a> — the function node expression.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let t = Tensor::new(Symbol::new("g"), vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("nu"), IndexPosition::Lower),
]);
let atom = t.to_atom(&ctx);
println!("{}", atom);  // g(mu, nu)
// Output: g(mu, nu)
}

See also: Atom


contract

Signature:

#![allow(unused)]
fn main() {
pub fn contract<'a>(
    ctx: &'a AtomArena<'a>,
    a: &Tensor<'a>,
    b: &Tensor<'a>,
) -> Contracted<'a>
}

Description: Contracts two tensors by summing over shared dummy indices. Two slots with the same label but opposite positions are contracted. The result retains the surviving free indices, in the order (a's free slots, b's free slots).

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
a&Tensor<'a>The first tensor
b&Tensor<'a>The second tensor

Returns: Contracted<'a>

  • With no contraction pair, returns Contracted::Product containing the two original tensors
  • With a partial contraction, returns Contracted::Product containing a new tensor carrying the free slots
  • With a full contraction (no free slots), returns Contracted::Scalar

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition, contract, Contracted};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// A^μ B_μ → scalar
let a = Tensor::new(Symbol::new("A"), vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
]);
let b = Tensor::new(Symbol::new("B"), vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Lower),
]);
match contract(&ctx, &a, &b) {
    Contracted::Scalar(expr) => println!("scalar: {}", expr),
    Contracted::Product(tp) => {
        for f in &tp.factors {
            println!("free: {} (rank {})", f.name().as_str(), f.rank());
        }
    }
}
// Output: scalar: A(mu)*B(mu)
}

See also: Contracted, TensorProduct


Contracted

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Contracted<'a> {
    Product(TensorProduct<'a>),
    Scalar(Atom<'a>),
}
}

Description: The result of contracting two tensors. When no free indices survive it is a scalar expression; otherwise it is a tensor product with free slots.

Variants:

VariantTypeDescription
ProductTensorProduct<'a>Partial or no contraction; a tensor product with free indices
ScalarAtom<'a>Full contraction to a scalar expression

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition, contract, Contracted};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// A^μ_ν B^ν_ρ → partial contraction, leaving A^μ_ρ
let a = Tensor::new(Symbol::new("A"), vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("nu"), IndexPosition::Lower),
]);
let b = Tensor::new(Symbol::new("B"), vec![
    IndexSlot::new(ctx.var("nu"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("rho"), IndexPosition::Lower),
]);
match contract(&ctx, &a, &b) {
    Contracted::Product(tp) => {
        assert_eq!(tp.factors.len(), 1);
        assert_eq!(tp.factors[0].rank(), 2); // mu, rho
    }
    Contracted::Scalar(_) => panic!("expected partial contraction"),
}
// Output: partial contraction, 2 free indices remain
}

TensorProduct

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TensorProduct<'a> {
    pub factors: Vec<Tensor<'a>>,
}
}

Description: A tensor product retaining the free slots after contraction. factors contains the tensors that survive the contraction, with slots concatenated in the order (a's free slots, b's free slots).

Fields:

FieldTypeDescription
factorsVec<Tensor<'a>>The list of tensor factors after contraction

See also: contract, Contracted


symmetrise_sign

Signature: pub fn symmetrise_sign(tensor: &Tensor<'_>) -> i64

Description: Sorts the tensor's index slots by label in ascending order and returns the sign of the antisymmetrization.

  • Symmetry::None or Symmetry::Symmetric: returns +1
  • Symmetry::Antisymmetric: returns the permutation parity (even permutation +1, odd permutation -1)

Note: This is not full canonicalization under the permutation group (that requires graph isomorphism); it merely provides a stable order for comparing equivalent symmetric tensors.

Parameters:

ParameterTypeDescription
tensor&Tensor<'_>The input tensor

Returns: i64 — +1 or -1.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition, Symmetry, symmetrise_sign};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let slots = vec![
    IndexSlot::new(ctx.var("a"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("b"), IndexPosition::Upper),
];
let anti = Tensor::new(Symbol::new("epsilon"), slots)
    .with_symmetry(Symmetry::Antisymmetric);

let sign = symmetrise_sign(&anti);
// If a < b the permutation is the identity, sign = +1
// Output: +1
}

SymmetrySpec

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Default)]
pub struct SymmetrySpec {
    pub symmetric_subsets: Vec<Vec<usize>>,
    pub antisymmetric_subsets: Vec<Vec<usize>>,
    pub cyclic: Option<Vec<usize>>,
}
}

Description: A fine-grained symmetry specification for tensors, used by the canonicalization engine. It is more flexible than the Symmetry enum — different subsets of slot positions can be assigned different symmetry behavior.

Fields:

FieldTypeDescription
symmetric_subsetsVec<Vec<usize>>List of symmetric slot subsets. Slots within the same subset are interchangeable
antisymmetric_subsetsVec<Vec<usize>>List of antisymmetric slot subsets. Swapping two slots within a subset flips the sign
cyclicOption<Vec<usize>>A slot subset undergoing cyclic permutations

SymmetrySpec::none

Signature: pub fn none() -> Self

Description: No symmetry — all slots are independent.

Returns: SymmetrySpec

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::SymmetrySpec;

let spec = SymmetrySpec::none();
assert!(spec.symmetric_subsets.is_empty());
assert!(spec.antisymmetric_subsets.is_empty());
assert!(spec.cyclic.is_none());
// Output: all assertions pass
}

SymmetrySpec::fully_symmetric

Signature: pub fn fully_symmetric(rank: usize) -> Self

Description: All slots are fully symmetric. Creates a symmetric subset containing all slots 0..rank.

Parameters:

ParameterTypeDescription
rankusizeThe rank of the tensor (total number of slots)

Returns: SymmetrySpec

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::SymmetrySpec;

let spec = SymmetrySpec::fully_symmetric(3);
assert_eq!(spec.symmetric_subsets, vec![vec![0, 1, 2]]);
// Output: symmetric subset = [[0, 1, 2]]
}

SymmetrySpec::fully_antisymmetric

Signature: pub fn fully_antisymmetric(rank: usize) -> Self

Description: All slots are fully antisymmetric. Creates an antisymmetric subset containing all slots 0..rank.

Parameters:

ParameterTypeDescription
rankusizeThe rank of the tensor (total number of slots)

Returns: SymmetrySpec

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::SymmetrySpec;

let spec = SymmetrySpec::fully_antisymmetric(4);
assert_eq!(spec.antisymmetric_subsets, vec![vec![0, 1, 2, 3]]);
// Output: antisymmetric subset = [[0, 1, 2, 3]]
}

SymmetrySpec::is_slot_hidden

Signature: pub fn is_slot_hidden(&self, pos: usize) -> bool

Description: Checks whether the given slot position should be "hidden" in the graph encoding (i.e. it does not participate in canonicalization comparisons). Symmetric and cyclic slots are hidden.

Parameters:

ParameterTypeDescription
posusizeThe slot position index

Returns: bool — true if the slot is hidden.

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::SymmetrySpec;

let spec = SymmetrySpec::fully_symmetric(2);
assert!(spec.is_slot_hidden(0));   // in symmetric subset
assert!(spec.is_slot_hidden(1));   // in symmetric subset

let none = SymmetrySpec::none();
assert!(!none.is_slot_hidden(0));  // not in any subset
// Output: all assertions pass
}

TensorRegistry

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Default)]
pub struct TensorRegistry { /* private fields */ }
}

Description: The complete registry for tensor canonicalization. It records which function heads are tensors and their symmetry specifications, and also manages the assignment of index dimension groups (preventing dummy-index renaming conflicts across dimensions).

TensorRegistry::new

Signature: pub fn new() -> Self

Description: Creates an empty registry.

Returns: TensorRegistry

TensorRegistry::register

Signature: pub fn register(&mut self, name: Symbol, spec: SymmetrySpec)

Description: Registers a tensor name together with its symmetry specification.

Parameters:

ParameterTypeDescription
nameSymbolThe tensor function head name
specSymmetrySpecThe symmetry specification

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry};
use ocas_atom::Symbol;

let mut reg = TensorRegistry::new();
reg.register(Symbol::new("g"), SymmetrySpec::fully_symmetric(2));
reg.register(Symbol::new("Riemann"), SymmetrySpec::none());
reg.register(Symbol::new("epsilon"), SymmetrySpec::fully_antisymmetric(4));
// Output: 3 tensors registered
}

TensorRegistry::set_index_group

Signature: pub fn set_index_group(&mut self, label: Symbol, group: u64)

Description: Sets the dimension-group identifier of an index label. Dummy indices from different groups are never renamed to the same canonical name, avoiding cross-dimension conflicts.

Parameters:

ParameterTypeDescription
labelSymbolThe index label (e.g. "mu", "i")
groupu64The group identifier. 0 means default/ungrouped

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::TensorRegistry;
use ocas_atom::Symbol;

let mut reg = TensorRegistry::new();
reg.set_index_group(Symbol::new("mu"), 1);  // spacetime index
reg.set_index_group(Symbol::new("i"), 2);   // internal index
assert_eq!(reg.index_group(Symbol::new("mu")), 1);
assert_eq!(reg.index_group(Symbol::new("i")), 2);
assert_eq!(reg.index_group(Symbol::new("other")), 0); // default group
// Output: all assertions pass
}

TensorRegistry::spec

Signature: pub fn spec(&self, name: Symbol) -> Option<&SymmetrySpec>

Description: Looks up the symmetry specification of a registered tensor.

Parameters:

ParameterTypeDescription
nameSymbolThe tensor name

Returns: Option<&SymmetrySpec> — None if not registered.

TensorRegistry::index_group

Signature: pub fn index_group(&self, label: Symbol) -> u64

Description: Looks up the dimension group of an index label. Labels that have not been set return 0.

Parameters:

ParameterTypeDescription
labelSymbolThe index label

Returns: u64 — the group identifier.

See also: SymmetrySpec


canonicalize_tensors

Signature:

#![allow(unused)]
fn main() {
pub fn canonicalize_tensors<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    registry: &TensorRegistry,
) -> Result<CanonicalTensor<'a>, TensorCanonError>
}

Description: Canonicalizes a tensor expression to a unique canonical form independent of index naming. Internally, the expression is encoded as a graph (tensor head → head vertex, index slot → slot vertex, contraction → edge), the McKay refinement-individualization algorithm computes canonical labels, and the expression is then rebuilt with renamed dummy indices.

For sum expressions (Add), each term is canonicalized individually and all terms are verified to have the same set of free indices.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
exprAtom<'a>The tensor expression to canonicalize (Mul of Fun nodes)
registry&TensorRegistryThe tensor registry (symmetry specifications + index groups)

Returns: Result<CanonicalTensor<'a>, TensorCanonError>

Errors:

Error variantDescription
ContractedMoreThanOnce(Symbol)An index label is contracted more than twice
BadContraction(Symbol)Two slots with the same label have the same position (not an upper/lower pair)
NotATensor(Symbol)A function head not registered in the registry appears in the expression
InconsistentOpenIndicesThe free indices of the terms in a sum are inconsistent
UnsupportedPowerAn unsupported power node appears in the expression

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry};
use ocas_atom::tensor::canon::canonicalize_tensors;
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let mut reg = TensorRegistry::new();
reg.register(Symbol::new("T"), SymmetrySpec::none());

// T(i, j) and T(j, i) are different (no symmetry)
let t_ij = ctx.fun("T", &[ctx.var("i"), ctx.var("j")]);
let t_ji = ctx.fun("T", &[ctx.var("j"), ctx.var("i")]);

let ct1 = canonicalize_tensors(&ctx, t_ij, &reg).unwrap();
let ct2 = canonicalize_tensors(&ctx, t_ji, &reg).unwrap();
// Without symmetry, T(i,j) ≠ T(j,i)
// Note: without symmetry, T(i,j) and T(j,i) have different canonical forms
}

See also: CanonicalTensor, TensorCanonError, TensorRegistry


CanonicalTensor

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct CanonicalTensor<'a> {
    pub canonical_form: Atom<'a>,
    pub external_indices: Vec<Atom<'a>>,
    pub dummy_indices: Vec<Atom<'a>>,
}
}

Description: The result of tensor canonicalization.

Fields:

FieldTypeDescription
canonical_formAtom<'a>The canonicalized expression (dummy indices renamed)
external_indicesVec<Atom<'a>>The list of free (external) indices
dummy_indicesVec<Atom<'a>>The list of dummy indices (renamed to d0, d1, …)

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry};
use ocas_atom::tensor::canon::canonicalize_tensors;
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let mut reg = TensorRegistry::new();
reg.register(Symbol::new("T"), SymmetrySpec::none());
reg.register(Symbol::new("U"), SymmetrySpec::none());

let prod = ctx.mul(&[ctx.fun("T", &[ctx.var("i"), ctx.var("j")]),
                      ctx.fun("U", &[ctx.var("j"), ctx.var("k")])]);
let ct = canonicalize_tensors(&ctx, prod, &reg).unwrap();

println!("Canonical form: {}", ct.canonical_form);
println!("External indices: {:?}", ct.external_indices);
println!("Dummy indices: {:?}", ct.dummy_indices);
// Output: canonical form contains renamed dummy indices; external indices are i, k
}

TensorCanonError

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TensorCanonError {
    ContractedMoreThanOnce(Symbol),
    BadContraction(Symbol),
    NotATensor(Symbol),
    InconsistentOpenIndices,
    UnsupportedPower,
}
}

Description: Errors that may arise during tensor canonicalization.

Variants:

VariantDescription
ContractedMoreThanOnce(Symbol)An index label is contracted more than twice (occurs 3 or more times)
BadContraction(Symbol)Two slots with the same label have the same variance (not a valid upper/lower pair)
NotATensor(Symbol)A function head not registered in the TensorRegistry appears
InconsistentOpenIndicesThe sets of free indices of the terms in a sum are inconsistent
UnsupportedPowerAn unsupported power node appears in the expression

refresh_dummies

Signature:

#![allow(unused)]
fn main() {
pub fn refresh_dummies<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    registry: &TensorRegistry,
) -> Result<Atom<'a>, DummyError>
}

Description: Renames the dummy indices in a tensor expression to avoid conflicts with external (free) indices. Labels that occur exactly twice (once as a superscript, once as a subscript) are replaced with new names assigned per dimension group (d0, d1, …). External indices remain unchanged.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
exprAtom<'a>The tensor expression to process
registry&TensorRegistryThe tensor registry (used for dimension-group assignment)

Returns: Result<Atom<'a>, DummyError>

Errors:

Error variantDescription
OverContracted(Symbol)An index label occurs more than twice
BadContraction(Symbol)Two slots with the same label have the same variance

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry};
use ocas_atom::tensor::dummy::refresh_dummies;
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let mut reg = TensorRegistry::new();
reg.register(Symbol::new("T"), SymmetrySpec::none());
reg.register(Symbol::new("U"), SymmetrySpec::none());

// T(i,j) * U(j,i): i and j both occur exactly twice → dummy indices
let expr = ctx.mul(&[ctx.fun("T", &[ctx.var("i"), ctx.var("j")]),
                      ctx.fun("U", &[ctx.var("j"), ctx.var("i")])]);
let refreshed = refresh_dummies(&ctx, expr, &reg).unwrap();
println!("{}", refreshed);
// Output: dummy indices renamed to d0, d1 (assigned per dimension group)
}

See also: DummyError, TensorRegistry


DummyError

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DummyError {
    OverContracted(Symbol),
    BadContraction(Symbol),
}
}

Description: The error type for dummy-index operations.

Variants:

VariantDescription
OverContracted(Symbol)An index label occurs more than twice (not a valid contraction candidate)
BadContraction(Symbol)Two slots with the same label have the same variance (not an upper/lower pair)

YoungTableau

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct YoungTableau {
    pub row_lengths: Vec<usize>,
}
}

Description: A Young tableau, whose shape is defined by a list of row lengths (e.g. [2, 1] denotes □□/□). The Young projector implements the classical symmetrizer $c_\lambda = a_\lambda \cdot b_\lambda$: it sums over all combinations of row permutations $\sigma \in R$ and column permutations $\tau \in C$, each term carrying the column permutation parity $\operatorname{sgn}(\tau)$.

This is an explicit expansion (not a BSGS group-theoretic implementation): the result is a sum of $\prod r_i! \cdot \prod c_j!$ terms (products of row/column factorials), each with sign ±1 and no hook-length normalization.

Fields:

FieldTypeDescription
row_lengthsVec<usize>The number of boxes in each row

YoungTableau::new

Signature: pub fn new(row_lengths: Vec<usize>) -> Self

Description: Creates a Young tableau from row lengths.

Parameters:

ParameterTypeDescription
row_lengthsVec<usize>The list of boxes per row. The total number of boxes should equal the rank of the tensor

Returns: YoungTableau

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::young::YoungTableau;

// [2, 1] = □□/□ (3 boxes)
let tab = YoungTableau::new(vec![2, 1]);
assert_eq!(tab.total_boxes(), 3);

// [1, 1, 1] = fully antisymmetric
let anti = YoungTableau::new(vec![1, 1, 1]);
assert_eq!(anti.total_boxes(), 3);

// [3] = fully symmetric
let sym = YoungTableau::new(vec![3]);
assert_eq!(sym.total_boxes(), 3);
// Output: all assertions pass
}

YoungTableau::total_boxes

Signature: pub fn total_boxes(&self) -> usize

Description: Returns the total number of boxes in the tableau (equal to the rank of the tensor).

Returns: usize


young_project

Signature:

#![allow(unused)]
fn main() {
pub fn young_project<'a>(
    ctx: &'a AtomArena<'a>,
    tensor_expr: Atom<'a>,
    tableau: &YoungTableau,
) -> Atom<'a>
}

Description: Applies the Young projector to a tensor expression. Expands $T(i_1, i_2, \dots, i_n)$ into a sum over permutations:

$$\sum_\sigma \text{sign}(\sigma) \cdot T(i_{\sigma(1)}, i_{\sigma(2)}, \dots, i_{\sigma(n)})$$

The current implementation does not normalize by the product of hook lengths — each term has coefficient $\pm 1$ only. For a fully antisymmetric tableau [1, 1, …, 1], the standard alternating sum is obtained.

Parameters:

ParameterTypeDescription
ctx&'a AtomArena<'a>The expression arena
tensor_exprAtom<'a>The tensor expression (a Fun node)
tableau&YoungTableauThe Young tableau shape

Returns: Atom<'a> — the projected expression (an Add node whose terms carry signs).

Example:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::young::{YoungTableau, young_project};
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// fully antisymmetric [1, 1]: f(a, b) → f(a, b) - f(b, a)
let f_ab = ctx.fun("f", &[ctx.var("a"), ctx.var("b")]);
let result = young_project(&ctx, f_ab, &YoungTableau::new(vec![1, 1]));
println!("{}", result);
// Output: f(a,b) - f(b,a)

// fully symmetric [2]: g(a, b) → g(a, b) + g(b, a)
let g_ab = ctx.fun("g", &[ctx.var("a"), ctx.var("b")]);
let result = young_project(&ctx, g_ab, &YoungTableau::new(vec![2]));
println!("{}", result);
// Output: g(a,b) + g(b,a)

// fully antisymmetric [1, 1, 1]: h(a, b, c) → alternating sum of 6 terms (coefficients ±1, not normalized)
let h = ctx.fun("h", &[ctx.var("a"), ctx.var("b"), ctx.var("c")]);
let result = young_project(&ctx, h, &YoungTableau::new(vec![1, 1, 1]));
println!("{}", result);
// Output: h(a,b,c) - h(b,a,c) - ... (6 terms, each with coefficient ±1)
}

See also: YoungTableau, SymmetrySpec


Design Invariants

Atom is a Copy arena handle

Atom<'a> is a Copy handle pointing to a node in the arena. IndexSlot<'a> is also Copy. This means tensors can be copied and compared cheaply — structural equality is equivalent to pointer equality (hash-consing).

Explicit index matching

The tensor algebra of oCAS uses explicit index matching rather than the Einstein summation convention. Contractions must be performed manually by calling contract; indices are paired by label and position (upper/lower).

Symmetry is advisory

The Symmetry enum and SymmetrySpec are metadata and are not used automatically by basic operations such as contract. Full symmetry handling requires:

  1. Canonicalization (canonicalize_tensors) — symmetry enforcement at the level of graph isomorphism
  2. Young projection (young_project) — explicit permutation expansion

Graph isomorphism engine

Canonicalization in 0.22.0 is based on the McKay refinement-individualization graph isomorphism algorithm (implemented in the tensor::graph module). This engine is a standalone nauty implementation that provides:

  • 1-WL color refinement (neighbor signatures + edge data + direction)
  • equitable partitions + individualization-refinement DFS
  • path-invariant pruning + automorphism orbits
  • canonical form = the lexicographically maximal certificate

Complete Example

use ocas_atom::tensor::{
    IndexPosition, IndexSlot, Symmetry, Tensor,
    contract, Contracted, symmetrise_sign,
};
use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry};
use ocas_atom::tensor::canon::canonicalize_tensors;
use ocas_atom::tensor::dummy::refresh_dummies;
use ocas_atom::tensor::young::{YoungTableau, young_project};
use ocas_atom::{AtomArena, Symbol};
use ocas_core::arena::Arena;

fn main() {
    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);

    // 1. Create the metric tensor g_μν with symmetry
    let g = Tensor::new(Symbol::new("g"), vec![
        IndexSlot::new(ctx.var("mu"), IndexPosition::Lower),
        IndexSlot::new(ctx.var("nu"), IndexPosition::Lower),
    ]).with_symmetry(Symmetry::Symmetric);
    println!("g rank = {}, symmetry = {:?}", g.rank(), g.symmetry());
    // Output: g rank = 2, symmetry = Symmetric

    // 2. Contract g^μν A_ν → partial contraction
    let g_inv = Tensor::new(Symbol::new("g"), vec![
        IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
        IndexSlot::new(ctx.var("nu"), IndexPosition::Upper),
    ]);
    let a = Tensor::new(Symbol::new("A"), vec![
        IndexSlot::new(ctx.var("nu"), IndexPosition::Lower),
    ]);
    match contract(&ctx, &g_inv, &a) {
        Contracted::Product(tp) => {
            println!("Free indices after contraction: {}", tp.factors[0].rank());
            // Output: number of free indices after contraction: 1 (mu)
        }
        _ => {}
    }

    // 3. Canonicalize: j in T(i,j)*U(j,k) is renamed
    let mut reg = TensorRegistry::new();
    reg.register(Symbol::new("T"), SymmetrySpec::none());
    reg.register(Symbol::new("U"), SymmetrySpec::none());

    let prod = ctx.mul(&[
        ctx.fun("T", &[ctx.var("i"), ctx.var("j")]),
        ctx.fun("U", &[ctx.var("j"), ctx.var("k")]),
    ]);
    let ct = canonicalize_tensors(&ctx, prod, &reg).unwrap();
    println!("Canonical form: {}", ct.canonical_form);
    // Output: canonical form contains dummy indices d0, d1, etc.

    // 4. Young projection: fully antisymmetrize f(a, b)
    let f = ctx.fun("f", &[ctx.var("a"), ctx.var("b")]);
    let anti = young_project(&ctx, f, &YoungTableau::new(vec![1, 1]));
    println!("Antisymmetrized: {}", anti);
    // Output: f(a,b) - f(b,a)
}

See also

Rust API Reference: Number Theory

This chapter documents the complete API of the oCAS number theory module ocas_domain::number_theory. The module provides primality testing, integer factorization, the Chinese remainder theorem, discrete logarithms, quadratic-residue symbols, and multiplicative number-theoretic functions. It is the infrastructure underlying polynomial factorization (Berlekamp, Cantor–Zassenhaus, Hensel lifting), rational reconstruction, and modular GCD algorithms.

Module path: ocas_domain::number_theory

Submodules:

SubmodulePurpose
primesPrimality testing, prime generation, modular inverse, extended Euclid, quadratic residues
factorComplete integer factorization (trial division, Pollard rho/p−1/p+1, ECM)
crtMulti-modulus Chinese remainder theorem
dlogDiscrete logarithms (BSGS, Pohlig–Hellman)
functionsMultiplicative number-theoretic functions (φ, μ, τ, σ_k, λ)

Imports:

#![allow(unused)]
fn main() {
use ocas_domain::number_theory::{
    is_prime, next_prime, primes_from, mod_inv, extended_gcd, symmetric_mod,
    crt, legendre, jacobi, mod_sqrt,
    factor::factor_integer,
    functions::{euler_phi, moebius_mu, divisor_tau, divisor_sigma, liouville_lambda},
};
// or import precisely from the submodules
use ocas_domain::number_theory::primes::is_prime_bpsw;
use ocas_domain::number_theory::primes::is_prime_u64;
use ocas_domain::number_theory::crt::crt_many;
use ocas_domain::number_theory::dlog::{dlog_bsgs, dlog_pohlig_hellman};
}

Primality testing

is_prime

Signature: pub fn is_prime(n: &Integer) -> bool

Description: Determines whether n is prime.

Parameters:

ParameterTypeDescription
n&IntegerThe integer under test

Return value: bool — true if n is a (strong) pseudoprime or a proven prime.

Implementation details:

  • For $n < 3.317 \times 10^{24}$, a deterministic test using the fixed set of 12 Miller–Rabin witnesses ${2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}$.
  • For larger n, falls back to a strong pseudoprime test (composite numbers pass with extremely low probability).
  • Handles $n \leq 3$ and even numbers explicitly.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::is_prime;

assert!(is_prime(&Integer::from(97)));
assert!(!is_prime(&Integer::from(561)));   // Carmichael number, not prime
assert!(is_prime(&Integer::from(2_147_483_647_i64))); // Mersenne prime M31
}

See also: is_prime_bpsw, is_prime_u64, next_prime


is_prime_bpsw

Signature: pub fn is_prime_bpsw(n: &Integer) -> bool

Description: BPSW pseudoprime test — base-2 strong Miller–Rabin plus a strong Lucas pseudoprime test (with Selfridge parameter selection).

Parameters:

ParameterTypeDescription
n&IntegerThe integer under test

Return value: bool — true if n passes the BPSW test.

Implementation details:

  1. First a base-2 strong Miller–Rabin test (shares the mr_witness core with is_prime).
  2. Then a strong Lucas test: Selfridge parameter selection — take the first $D$ from the sequence $5, -7, 9, -11, \ldots$ with $\text{jacobi}(D, n) = -1$, and set $P = 1$, $Q = (1 - D)/4$.
  3. Write $n + 1 = d \cdot 2^r$ ($d$ odd) and compute the Lucas sequences $(U_k, V_k)$ with a binary ladder. n passes when $U_d \equiv 0$ or $V_{d \cdot 2^i} \equiv 0 \pmod{n}$ for some $0 \leq i < r$.

Known properties: No composite number is currently known to pass the BPSW test (as of 2026). For $n < 3.317 \times 10^{24}$ the result is deterministic.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::primes::is_prime_bpsw;

assert!(is_prime_bpsw(&Integer::from(97)));
assert!(!is_prime_bpsw(&Integer::from(561)));    // Carmichael number
assert!(!is_prime_bpsw(&Integer::from(2047)));   // base-2 strong pseudoprime, but not prime
}

See also: is_prime, factor_integer (uses BPSW internally as the final primality check)


is_prime_u64

Signature: pub fn is_prime_u64(n: u64) -> bool

Description: Deterministic primality test for integers in the u64 range.

Parameters:

ParameterTypeDescription
nu64The integer under test

Return value: bool.

Implementation details: converts n to an Integer internally and calls is_prime. The 12 Miller–Rabin witnesses cover the entire u64 range ($< 3.317 \times 10^{24}$), so the result is deterministic.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::number_theory::primes::is_prime_u64;

assert!(is_prime_u64(97));
assert!(!is_prime_u64(561));
assert!(is_prime_u64(u64::MAX - 58)); // 2^64 − 59, the largest u64 prime
}

See also: is_prime


Prime generation

next_prime

Signature: pub fn next_prime(n: &Integer) -> Integer

Description: Returns the smallest prime strictly greater than n.

Parameters:

ParameterTypeDescription
n&IntegerThe starting integer

Return value: Integer — the smallest prime greater than n.

Implementation details: starts from $n + 1$ (or from 2 if $n < 2$) and tests odd candidates in turn.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::next_prime;

assert_eq!(next_prime(&Integer::from(10)), Integer::from(11));
assert_eq!(next_prime(&Integer::from(13)), Integer::from(17));
assert_eq!(next_prime(&Integer::from(0)), Integer::from(2));
}

See also: primes_from, is_prime


primes_from

Signature: pub fn primes_from(n: &Integer) -> PrimesFrom

Description: Creates an iterator over the consecutive primes after n.

Parameters:

ParameterTypeDescription
n&IntegerThe starting integer

Return value: PrimesFrom — an iterator implementing Iterator<Item = Integer> that yields the primes strictly greater than n in order.

Use cases: scanning for primes in Hensel lifting (looking for a prime that does not divide the leading coefficient and keeps $f \bmod p$ square-free).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::primes_from;

let mut it = primes_from(&Integer::from(100));
assert_eq!(it.next().unwrap().to_string(), "101");
assert_eq!(it.next().unwrap().to_string(), "103");
}

See also: next_prime


Modular arithmetic

mod_inv

Signature: pub fn mod_inv(a: &Integer, m: &Integer) -> Option<Integer>

Description: Computes the multiplicative inverse of a modulo m, i.e. an x such that $a \cdot x \equiv 1 \pmod{m}$.

Parameters:

ParameterTypeDescription
a&IntegerThe element to invert
m&IntegerThe modulus

Return value: Option<Integer> — Some(x) with $0 \leq x < m$, or None.

Error conditions: returns None when $\gcd(a, m) \neq 1$ or $m \leq 1$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::mod_inv;

assert_eq!(mod_inv(&Integer::from(3), &Integer::from(11)), Some(Integer::from(4)));
// 3 × 4 = 12 ≡ 1 (mod 11)
assert_eq!(mod_inv(&Integer::from(2), &Integer::from(4)), None);
// gcd(2, 4) = 2 ≠ 1, the inverse does not exist
}

See also: extended_gcd


extended_gcd

Signature: pub fn extended_gcd(a: &Integer, b: &Integer) -> (Integer, Integer, Integer)

Description: Extended Euclid algorithm — computes $g = \gcd(a, b)$ and Bézout coefficients $x, y$ such that $g = a \cdot x + b \cdot y$.

Parameters:

ParameterTypeDescription
a&IntegerThe first integer
b&IntegerThe second integer

Return value: (g, x, y) — g is non-negative and satisfies $g = a \cdot x + b \cdot y$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::extended_gcd;

let (g, x, y) = extended_gcd(&Integer::from(240), &Integer::from(46));
assert_eq!(g, Integer::from(2));
// Verify the Bézout identity: 240·x + 46·y = 2
assert_eq!(&x * &Integer::from(240) + &y * &Integer::from(46), g);
}

See also: mod_inv


symmetric_mod

Signature: pub fn symmetric_mod(a: &Integer, m: &Integer) -> Integer

Description: Reduces a to the symmetric interval $(-m/2, m/2]$.

Parameters:

ParameterTypeDescription
a&IntegerThe integer to reduce
m&IntegerThe modulus (positive)

Return value: Integer — the representative in $(-m/2, m/2]$.

Use cases: recovering integer coefficients from modular representations in Hensel lifting.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::symmetric_mod;

// mod 7, interval (-3.5, 3.5]
assert_eq!(symmetric_mod(&Integer::from(3), &Integer::from(7)), Integer::from(3));
assert_eq!(symmetric_mod(&Integer::from(5), &Integer::from(7)), Integer::from(-2));
assert_eq!(symmetric_mod(&Integer::from(6), &Integer::from(7)), Integer::from(-1));
}

See also: crt


Chinese remainder theorem

crt

Signature: pub fn crt(r1: &Integer, m1: &Integer, r2: &Integer, m2: &Integer) -> Option<(Integer, Integer)>

Description: Combines two congruences $x \equiv r_1 \pmod{m_1}$ and $x \equiv r_2 \pmod{m_2}$.

Parameters:

ParameterTypeDescription
r1&IntegerThe first remainder
m1&IntegerThe first modulus
r2&IntegerThe second remainder
m2&IntegerThe second modulus

Return value: Option<(Integer, Integer)> — Some((r, m)) where $m = \operatorname{lcm}(m_1, m_2)$, $0 \leq r < m$, $r \equiv r_1 \pmod{m_1}$, and $r \equiv r_2 \pmod{m_2}$.

Error conditions: returns None when the system is inconsistent ($r_1 - r_2$ not divisible by $\gcd(m_1, m_2)$). The moduli need not be coprime.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::crt;

// x ≡ 2 (mod 3), x ≡ 3 (mod 5)  =>  x ≡ 8 (mod 15)
let (r, m) = crt(&Integer::from(2), &Integer::from(3),
                 &Integer::from(3), &Integer::from(5)).unwrap();
assert_eq!(r, Integer::from(8));
assert_eq!(m, Integer::from(15));
}

See also: crt_many


crt_many

Signature: pub fn crt_many(congruences: &[(Integer, Integer)]) -> Option<(Integer, Integer)>

Description: Combines multiple congruences $x \equiv r_i \pmod{m_i}$.

Parameters:

ParameterTypeDescription
congruences&[(Integer, Integer)]The list of congruences, each an (r_i, m_i) pair

Return value: Option<(Integer, Integer)> — Some((R, M)) where $M = \operatorname{lcm}(m_1, \ldots, m_k)$ and $0 \leq R < M$.

Error conditions:

  • Returns None for an empty list.
  • Returns None when any pair is inconsistent.

Implementation details: folds pairwise via crt; the moduli need not be pairwise coprime.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::crt::crt_many;

// Sunzi Suanjing problem: x ≡ 2 (mod 3), x ≡ 3 (mod 5), x ≡ 2 (mod 7)
let cs = [
    (Integer::from(2), Integer::from(3)),
    (Integer::from(3), Integer::from(5)),
    (Integer::from(2), Integer::from(7)),
];
let (r, m) = crt_many(&cs).unwrap();
assert_eq!(r, Integer::from(23));
assert_eq!(m, Integer::from(105)); // 3 × 5 × 7 = 105
}

See also: crt, dlog_pohlig_hellman (uses CRT internally to combine partial results)


Quadratic residues

legendre

Signature: pub fn legendre(a: &Integer, p: &Integer) -> i8

Description: Computes the Legendre symbol $\left(\frac{a}{p}\right)$.

Parameters:

ParameterTypeDescription
a&IntegerThe integer under test
p&IntegerAn odd prime (primality is guaranteed by the caller)

Return value:

ValueMeaning
1$a$ is a quadratic residue (QR) modulo $p$
-1$a$ is a quadratic non-residue (QNR) modulo $p$
0$p \mid a$

Note: primality of p is guaranteed by the caller; the function is internally equivalent to jacobi(a, p).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::legendre;

assert_eq!(legendre(&Integer::from(2), &Integer::from(7)), 1);  // 2 is a QR mod 7
assert_eq!(legendre(&Integer::from(3), &Integer::from(7)), -1); // 3 is a QNR mod 7
}

See also: jacobi, mod_sqrt


jacobi

Signature: pub fn jacobi(a: &Integer, n: &Integer) -> i8

Description: Computes the Jacobi symbol $\left(\frac{a}{n}\right)$ — a generalization of the Legendre symbol, defined for any positive odd n.

Parameters:

ParameterTypeDescription
a&IntegerThe integer under test
n&IntegerA positive odd integer

Return value: i8 — 0, 1, or -1.

Implementation details: computed via quadratic reciprocity, including 2-adic stripping and the mod-8/mod-4 sign rules.

Note: if n is even or non-positive, the result is undefined (the function returns 0).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::jacobi;

assert_eq!(jacobi(&Integer::from(2), &Integer::from(15)), 1);
assert_eq!(jacobi(&Integer::from(7), &Integer::from(15)), -1);
}

See also: legendre


mod_sqrt

Signature: pub fn mod_sqrt(a: &Integer, p: &Integer) -> Option<Integer>

Description: Computes a square root of $a$ modulo the odd prime $p$, i.e. an $x$ such that $x^2 \equiv a \pmod{p}$.

Parameters:

ParameterTypeDescription
a&IntegerThe radicand
p&IntegerAn odd prime

Return value: Option<Integer> — Some(x) with $0 \leq x < p$ (the other root is $p - x$).

Error conditions: returns None when $p \leq 2$ or when $a$ is a quadratic non-residue (legendre(a, p) = -1). The primality of p is the caller's responsibility — the function does not check it, and behavior for odd composite p is undefined.

Implementation details:

  • Fast path $x = a^{(p+1)/4} \bmod p$ when $p \equiv 3 \pmod{4}$.
  • Otherwise the full Tonelli–Shanks algorithm: find a non-residue $z$, maintaining the invariants $c, t, r, m$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::mod_sqrt;

// 2 is a QR mod 7: roots are 3 and 4 (3² = 9 ≡ 2, 4² = 16 ≡ 2 mod 7)
let r = mod_sqrt(&Integer::from(2), &Integer::from(7)).unwrap();
assert!(r == Integer::from(3) || r == Integer::from(4));
}

See also: legendre


Integer factorization

factor_integer

Signature: pub fn factor_integer(n: &Integer) -> Vec<(Integer, u32)>

Description: Completely factors $|n|$ into prime factors, returning (prime, exponent) pairs in ascending order.

Parameters:

ParameterTypeDescription
n&IntegerThe integer to factor

Return value: Vec<(Integer, u32)> — the list of prime factors, in ascending order of prime. Returns an empty list for $n \in {0, \pm 1}$.

Factorization strategy:

  1. Trial division (factor_trial): removes small factors $\leq 1000$.
  2. For the remaining composite cofactor, an escalating strategy that tries, in stages:
    • Pollard rho–Brent variant (batched gcd + backtracking)
    • Pollard $p-1$ stage 1 (smoothness bound doubling by $\times 4$)
    • Williams $p+1$ stage 1 (Lucas $V$ sequences, random $P$ with $\text{jacobi}(P^2-4, n) = -1$)
    • ECM Lenstra (Suyama parametrization, Montgomery curves in projective coordinates, curve budget $\approx B_1/550$)
  3. Every leaf is verified with the BPSW primality test.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::factor::factor_integer;

let f = factor_integer(&Integer::from(2 * 2 * 3 * 5 * 101 * 1000003));
assert_eq!(f, vec![
    (Integer::from(2), 2),
    (Integer::from(3), 1),
    (Integer::from(5), 1),
    (Integer::from(101), 1),
    (Integer::from(1000003), 1),
]);

// empty list
assert!(factor_integer(&Integer::from(0)).is_empty());
assert!(factor_integer(&Integer::from(1)).is_empty());
}

See also: is_prime_bpsw, euler_phi, moebius_mu


factor_integer_with_rng

Signature: pub fn factor_integer_with_rng(n: &Integer, rng: &mut Xoshiro256PlusPlus) -> Vec<(Integer, u32)>

Description: Same as factor_integer, but accepts an explicit RNG for reproducible factorization results.

Parameters:

ParameterTypeDescription
n&IntegerThe integer to factor
rng&mut Xoshiro256PlusPlusThe random number generator (from the rand_xoshiro crate)

Return value: Vec<(Integer, u32)>.

Use cases: when deterministic results are needed in tests.

See also: factor_integer


factor_trial

Signature: pub fn factor_trial(n: &Integer, limit: u64) -> (Vec<(Integer, u32)>, Integer)

Description: Trial division that removes prime factors $\leq \text{limit}$.

Parameters:

ParameterTypeDescription
n&IntegerThe integer to factor
limitu64The trial-division bound

Return value: (factors, cofactor) — factors is the list of factors found, and cofactor has no prime factor $\leq \text{limit}$ (but is not necessarily prime).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::factor::factor_trial;

let (factors, rest) = factor_trial(&Integer::from(2 * 2 * 3 * 7 * 1000003), 100);
assert_eq!(factors, vec![
    (Integer::from(2), 2),
    (Integer::from(3), 1),
    (Integer::from(7), 1),
]);
assert_eq!(rest, Integer::from(1000003));
}

See also: factor_integer


pollard_rho_brent

Signature: pub fn pollard_rho_brent(n: &Integer, rng: &mut Xoshiro256PlusPlus) -> Option<Integer>

Description: Pollard rho in the Brent variant — searches for a non-trivial factor of an odd composite n.

Parameters:

ParameterTypeDescription
n&IntegerAn odd composite number
rng&mut Xoshiro256PlusPlusThe random number generator

Return value: Option<Integer> — Some(d) is a non-trivial factor of n; None means all retries failed (very rare).

Implementation details: a batched-gcd + backtracking strategy improves the success rate, with a bounded number of retries.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::factor::pollard_rho_brent;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;

let n = Integer::from(1000003) * Integer::from(1000033);
let mut rng = Xoshiro256PlusPlus::seed_from_u64(42);
let d = pollard_rho_brent(&n, &mut rng).unwrap();
assert!(d == Integer::from(1000003) || d == Integer::from(1000033));
}

See also: factor_integer


pollard_pm1

Signature: pub fn pollard_pm1(n: &Integer, b1: u64, rng: &mut Xoshiro256PlusPlus) -> Option<Integer>

Description: Pollard $p-1$ method (stage 1) — finds a factor p of n when $p-1$ is $B_1$-smooth.

Parameters:

ParameterTypeDescription
n&IntegerAn odd composite number
b1u64The smoothness bound
rng&mut Xoshiro256PlusPlusThe random number generator

Return value: Option<Integer>.

Implementation details: computes $a^M \bmod n$ where $M = \prod q^e \leq B_1$ ($q$ ranging over primes with $q^e \leq B_1$), periodically testing $\gcd(a^M - 1, n)$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::factor::pollard_pm1;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;

// 65537 is prime, 65536 = 2^16 is 2^17-smooth
let n = Integer::from(65537) * Integer::from(1000003);
let mut rng = Xoshiro256PlusPlus::seed_from_u64(7);
let d = pollard_pm1(&n, 1 << 17, &mut rng).unwrap();
assert_eq!(n.mod_floor(&d), Integer::from(0));
}

See also: williams_pp1, ecm


williams_pp1

Signature: pub fn williams_pp1(n: &Integer, b1: u64, rng: &mut Xoshiro256PlusPlus) -> Option<Integer>

Description: Williams $p+1$ method (stage 1) — finds a factor p of n when $p+1$ is $B_1$-smooth.

Parameters:

ParameterTypeDescription
n&IntegerAn odd composite number
b1u64The smoothness bound
rng&mut Xoshiro256PlusPlusThe random number generator

Return value: Option<Integer>.

Implementation details: uses Lucas $V$ sequences ($Q = 1$) with a random $P$ chosen so that $\text{jacobi}(P^2 - 4, n) = -1$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::factor::williams_pp1;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;

// 31 is prime, 31 + 1 = 2^5 is 64-smooth
let n = Integer::from(31) * Integer::from(1000003);
let mut rng = Xoshiro256PlusPlus::seed_from_u64(13);
let d = williams_pp1(&n, 64, &mut rng).unwrap();
assert_eq!(n.mod_floor(&d), Integer::from(0));
}

See also: pollard_pm1, ecm


ecm

Signature: pub fn ecm(n: &Integer, b1: u64, max_curves: u32, rng: &mut Xoshiro256PlusPlus) -> Option<Integer>

Description: Lenstra's elliptic curve method (ECM, stage 1) — finds a factor p of n when the group order of some curve over $\mathbb{F}_p$ is $B_1$-smooth.

Parameters:

ParameterTypeDescription
n&IntegerAn odd composite number
b1u64The smoothness bound
max_curvesu32The maximum number of curves to try
rng&mut Xoshiro256PlusPlusThe random number generator

Return value: Option<Integer>.

Implementation details:

  • Suyama parametrization: build a Montgomery curve from $\sigma \notin {0, 1, 5}$ with $a24 = (A+2)/4$ and $A = \frac{(v-u)^3(3u+v)}{4u^3v} - 2$, where $u = \sigma^2 - 5$ and $v = 4\sigma$.
  • Montgomery ladder scalar multiplication in projective coordinates $(X:Z)$.
  • Curve budget $\approx B_1 / 550$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::factor::ecm;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;

let n = Integer::from(1000003) * Integer::from(1000033);
let mut rng = Xoshiro256PlusPlus::seed_from_u64(99);
let d = ecm(&n, 2_000, 50, &mut rng).unwrap();
assert!(d == Integer::from(1000003) || d == Integer::from(1000033));
}

See also: factor_integer, pollard_pm1


Discrete logarithms

dlog_bsgs

Signature: pub fn dlog_bsgs(base: &Integer, target: &Integer, modulus: &Integer) -> Option<Integer>

Description: Solves $base^x \equiv \text{target} \pmod{\text{modulus}}$ with the baby-step giant-step (BSGS) algorithm, searching $x < \text{modulus}$.

Parameters:

ParameterTypeDescription
base&IntegerThe base
target&IntegerThe target value
modulus&IntegerThe modulus

Return value: Option<Integer> — Some(x) with $0 \leq x < \text{modulus}$, or None if no solution exists.

Error conditions:

  • $\gcd(\text{base}, \text{modulus}) \neq 1$ (the base is not a unit).
  • No such $x$ exists.

Complexity: $O(\sqrt{\text{modulus}})$ time and space. Only suitable for small modulus.

Implementation details: a HashMap stores the baby steps; giant-step lookups take $O(\sqrt{m})$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::dlog::dlog_bsgs;

// 2 is a primitive root mod 11; 2^7 = 128 ≡ 7 (mod 11)
let x = dlog_bsgs(&Integer::from(2), &Integer::from(7), &Integer::from(11)).unwrap();
assert_eq!(Integer::from(2).modpow(&x, &Integer::from(11)), Integer::from(7));
}

See also: dlog_pohlig_hellman


dlog_pohlig_hellman

Signature: pub fn dlog_pohlig_hellman(base: &Integer, target: &Integer, p: &Integer) -> Option<Integer>

Description: Solves $base^x \equiv \text{target} \pmod{p}$ with the Pohlig–Hellman algorithm ($p$ prime).

Parameters:

ParameterTypeDescription
base&IntegerThe base
target&IntegerThe target value
p&IntegerThe prime modulus

Return value: Option<Integer> — Some(x), or None if there is no solution or the input does not satisfy the preconditions.

Error conditions:

  • p is composite.
  • base is not a unit modulo p.
  • target is not in the subgroup generated by base.

Implementation details:

  1. Factor the order of base with factor_integer.
  2. For each prime power $q^e$, recover the discrete logarithm digit by digit with BSGS (digit recovery).
  3. Combine the partial results with crt_many.
  4. Finally verify $\text{base}^x \equiv \text{target} \pmod{p}$.

Complexity: dominated by the largest prime factor $q$, giving $O(\sqrt{q})$. Extremely efficient when the order is smooth.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::dlog::dlog_pohlig_hellman;

// p = 101, p − 1 = 2²·5² (smooth). 2 is a primitive root mod 101.
let p = Integer::from(101);
let base = Integer::from(2);
let target = base.modpow(&Integer::from(83), &p);
let x = dlog_pohlig_hellman(&base, &target, &p).unwrap();
assert_eq!(x, Integer::from(83));
}

See also: dlog_bsgs, factor_integer, crt_many


Multiplicative number-theoretic functions

All multiplicative functions are computed by factoring via factor_integer and combining the results.

euler_phi

Signature: pub fn euler_phi(n: &Integer) -> Integer

Description: Euler's totient function $\varphi(n)$ — the number of integers in $[1, |n|]$ coprime to $n$.

Parameters:

ParameterTypeDescription
n&IntegerThe input integer

Return value: Integer — $\varphi(|n|)$.

Formula: $|n| \cdot \prod_{p \mid n} \left(1 - \frac{1}{p}\right)$, over the distinct prime factors of n.

Convention: $\varphi(0) = 0$, $\varphi(\pm 1) = 1$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::functions::euler_phi;

assert_eq!(euler_phi(&Integer::from(9)), Integer::from(6));
// φ(9) = 9 × (1 − 1/3) = 6, numbers coprime to 9 are {1,2,4,5,7,8}
assert_eq!(euler_phi(&Integer::from(36)), Integer::from(12));
assert_eq!(euler_phi(&Integer::from(97)), Integer::from(96)); // prime p: φ(p) = p−1
}

See also: factor_integer


moebius_mu

Signature: pub fn moebius_mu(n: &Integer) -> i8

Description: The Möbius function $\mu(n)$.

Parameters:

ParameterTypeDescription
n&IntegerThe input integer

Return value: i8.

Definition:

$$ \mu(n) = \begin{cases} 1 & n = \pm 1 \ (-1)^k & n \text{ has } k \text{ distinct prime factors (square-free)} \ 0 & n \text{ is divisible by the square of a prime} \end{cases} $$

Convention: $\mu(0) = 0$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::functions::moebius_mu;

assert_eq!(moebius_mu(&Integer::from(1)), 1);
assert_eq!(moebius_mu(&Integer::from(6)), 1);   // 6 = 2·3, 2 prime factors → (−1)² = 1
assert_eq!(moebius_mu(&Integer::from(30)), -1); // 30 = 2·3·5, 3 prime factors → (−1)³ = −1
assert_eq!(moebius_mu(&Integer::from(12)), 0);  // 4 = 2² divides 12
}

See also: factor_integer


divisor_tau

Signature: pub fn divisor_tau(n: &Integer) -> Integer

Description: The divisor function $\tau(n)$ (also written $d(n)$) — the number of positive divisors of $|n|$.

Parameters:

ParameterTypeDescription
n&IntegerThe input integer

Return value: Integer — $\tau(|n|)$.

Formula: if $n = \prod p_i^{e_i}$, then $\tau(n) = \prod (e_i + 1)$.

Convention: $\tau(0) = 0$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::functions::divisor_tau;

assert_eq!(divisor_tau(&Integer::from(12)), Integer::from(6));
// 12 = 2²·3¹ → (2+1)(1+1) = 6, divisors are {1,2,3,4,6,12}
assert_eq!(divisor_tau(&Integer::from(97)), Integer::from(2)); // a prime has only 1 and itself
}

See also: divisor_sigma, factor_integer


divisor_sigma

Signature: pub fn divisor_sigma(n: &Integer, k: u32) -> Integer

Description: The divisor function $\sigma_k(n)$ — the sum of the $k$-th powers of the positive divisors of $|n|$.

Parameters:

ParameterTypeDescription
n&IntegerThe input integer
ku32The power (non-negative integer)

Return value: Integer — $\sigma_k(|n|)$.

Formula: $\sigma_k(n) = \prod \frac{p_i^{k(e_i+1)} - 1}{p_i^k - 1}$.

Special values:

  • $\sigma_0(n) = \tau(n)$ (number of divisors)
  • $\sigma_1(n) = \sigma(n)$ (sum of divisors — perfect-number test: $n$ is perfect if and only if $\sigma_1(n) = 2n$)

Convention: $\sigma_k(0) = 0$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::functions::divisor_sigma;

assert_eq!(divisor_sigma(&Integer::from(12), 1), Integer::from(28));
// σ₁(12) = 1+2+3+4+6+12 = 28
assert_eq!(divisor_sigma(&Integer::from(12), 2), Integer::from(210));
// σ₂(12) = 1+4+9+16+36+144 = 210
assert_eq!(divisor_sigma(&Integer::from(12), 0), Integer::from(6));
// σ₀(12) = τ(12) = 6
}

See also: divisor_tau, factor_integer


liouville_lambda

Signature: pub fn liouville_lambda(n: &Integer) -> i8

Description: The Liouville function $\lambda(n) = (-1)^{\Omega(n)}$, where $\Omega(n)$ is the total number of prime factors counted with multiplicity.

Parameters:

ParameterTypeDescription
n&IntegerThe input integer

Return value: i8 — 1 or -1.

Convention: $\lambda(0) = 0$, $\lambda(\pm 1) = 1$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::functions::liouville_lambda;

assert_eq!(liouville_lambda(&Integer::from(12)), -1);
// 12 = 2²·3, Ω = 3 → (−1)³ = −1
assert_eq!(liouville_lambda(&Integer::from(6)), 1);
// 6 = 2·3, Ω = 2 → (−1)² = 1
}

See also: factor_integer, moebius_mu


Module dependency graph

graph TD
    subgraph primes["primes module"]
        is_prime
        is_prime_bpsw
        is_prime_u64
        next_prime
        primes_from
        mod_inv
        extended_gcd
        symmetric_mod
        crt
        legendre
        jacobi
        mod_sqrt
    end

    subgraph factor["factor module"]
        factor_integer
        factor_trial
        pollard_rho_brent
        pollard_pm1
        williams_pp1
        ecm
    end

    subgraph crt_mod["crt module"]
        crt_many
    end

    subgraph dlog["dlog module"]
        dlog_bsgs
        dlog_pohlig_hellman
    end

    subgraph functions["functions module"]
        euler_phi
        moebius_mu
        divisor_tau
        divisor_sigma
        liouville_lambda
    end

    factor_integer --> is_prime_bpsw
    factor_integer --> factor_trial
    factor_integer --> pollard_rho_brent
    factor_integer --> pollard_pm1
    factor_integer --> williams_pp1
    factor_integer --> ecm
    is_prime --> mr_witness
    is_prime_bpsw --> is_prime
    is_prime_u64 --> is_prime
    next_prime --> is_prime
    primes_from --> next_prime
    crt_many --> crt
    dlog_pohlig_hellman --> factor_integer
    dlog_pohlig_hellman --> dlog_bsgs
    dlog_pohlig_hellman --> crt_many
    euler_phi --> factor_integer
    moebius_mu --> factor_integer
    divisor_tau --> factor_integer
    divisor_sigma --> factor_integer
    liouville_lambda --> factor_integer
    williams_pp1 --> jacobi
    legendre --> jacobi
    mod_sqrt --> legendre

Gröbner Bases and Ideals

This chapter documents the complete Rust API for Gröbner basis computation and ideal operations in oCAS. All algorithms are accessed through the unified entry point [groebner_basis]; ideal operations live in the ocas_poly::ideal module.

Table of contents


Algorithm

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Algorithm {
    Auto,
    F4,
    F5,
    Buchberger,
}
}

Description: Algorithm selector for Gröbner basis computation; passed to the unified entry point [groebner_basis].

Variants:

VariantDescription
Auto (default)Automatically selects the algorithm based on the size and structure of the ideal. Currently routes to F4; the F5 switching threshold will be tuned against the cyclic-n benchmarks in the future.
F4Forces the F4 matrix algorithm (Faugère 1999). Batches S-polynomial reduction into sparse matrix row operations, significantly faster than Buchberger for larger ideals.
F5Forces the F5 signature algorithm (Faugère 2002). Rejects zero reductions before matrix construction via the syzygy criterion, giving order-of-magnitude speedups on hard ideals (e.g., cyclic-n).
BuchbergerForces the classic Buchberger S-polynomial iteration algorithm; suitable for small ideals.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, groebner_basis, SparseMultivariatePolynomial};

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb = groebner_basis(&[f1, f2], Algorithm::F4);
assert!(gb.is_groebner_basis());
}

See also: groebner_basis


GroebnerBasis

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GroebnerBasis<D: Domain, O: MonomialOrder> {
    pub basis: Vec<SparseMultivariatePolynomial<D, O>>,
}
}

Description: A Gröbner basis of a polynomial ideal. The basis field stores the list of polynomials of the basis. Constructed via buchberger, f4::f4, f5::f5, or groebner_basis; all entry points return a reduced Gröbner basis (minimized + auto-reduced).

Type parameters:

ParameterConstraintDescription
DDomainCoefficient domain (e.g., RationalDomain, FiniteField)
OMonomialOrderMonomial order (e.g., Lex, Grevlex)

Fields:

FieldTypeDescription
basisVec<SparseMultivariatePolynomial<D, O>>List of polynomials of the basis

GroebnerBasis::buchberger

#![allow(unused)]
fn main() {
pub fn buchberger(ideal: &[SparseMultivariatePolynomial<D, O>]) -> Self
}

Description: Computes a Gröbner basis from the generators using the Buchberger algorithm. Internally filters out zero polynomials, applies Buchberger's first criterion (skips S-polynomials whose leading terms are coprime), with a maximum of 10,000 iterations.

Parameters:

ParameterTypeDescription
ideal&[SparseMultivariatePolynomial<D, O>]List of generators of the ideal

Returns: GroebnerBasis<D, O> — the raw basis, not minimized/auto-reduced.

Note: Requires the coefficient domain to support exact division (i.e., to be a field). Panics if division fails. The convenience function buchberger (a free function) additionally calls minimize().auto_reduce().

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{GroebnerBasis, SparseMultivariatePolynomial};

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb = GroebnerBasis::buchberger(&[f1, f2]);
assert!(gb.basis.len() >= 2);
}

See also: buchberger (convenience free function), f4::f4, f5::f5

minimize

#![allow(unused)]
fn main() {
pub fn minimize(mut self) -> Self
}

Description: Minimizes the basis — removes polynomials whose leading monomial is divisible by the leading monomial of another element.

Returns: Self — the minimized basis (consumes self).

See also: auto_reduce

auto_reduce

#![allow(unused)]
fn main() {
pub fn auto_reduce(mut self) -> Self
}

Description: Auto-reduces the basis — reduces each element against the others and makes it monic. Processes elements in ascending order of leading monomial, guaranteeing the standard reduced Gröbner basis property: no monomial of any basis element is divisible by the leading monomial of another basis element.

Returns: Self — the reduced basis (consumes self).

See also: minimize

is_groebner_basis

#![allow(unused)]
fn main() {
pub fn is_groebner_basis(&self) -> bool
}

Description: Verifies that this basis is indeed a Gröbner basis — checks that all S-polynomials reduce to zero.

Returns: bool — true if it is a valid Gröbner basis.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, SparseMultivariatePolynomial, groebner_basis};

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb = groebner_basis(&[f1, f2], Algorithm::Auto);
assert!(gb.is_groebner_basis());
}

See also: groebner_basis

reorder

#![allow(unused)]
fn main() {
pub fn reorder<O2: MonomialOrder>(&self) -> GroebnerBasis<D, O2>
where
    D: 'static,
}

Description: Changes the monomial order of the Gröbner basis. Reinterprets the polynomials under the target order and re-runs F4. This is the simple order-change path; for zero-dimensional ideals, use fglm for the faster $O(n \cdot D^3)$ conversion.

Type parameters:

ParameterDescription
O2Target monomial order

Returns: GroebnerBasis<D, O2> — a Gröbner basis under the target order.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::{Grevlex, Lex};
use ocas_poly::{GroebnerBasis, SparseMultivariatePolynomial, f4};

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb_lex = f4::f4(&[f1, f2]);
let gb_grevlex = gb_lex.reorder::<Grevlex>();
assert!(gb_grevlex.is_groebner_basis());
}

See also: fglm (faster order change for zero-dimensional ideals)


groebner_basis

#![allow(unused)]
fn main() {
pub fn groebner_basis<D: Domain + 'static, O: MonomialOrder>(
    ideal: &[SparseMultivariatePolynomial<D, O>],
    algo: Algorithm,
) -> GroebnerBasis<D, O>
}

Description: Unified entry point for Gröbner basis computation. Selects the algorithm according to the algo parameter (Auto/F4/F5/Buchberger). Zero polynomials are filtered out internally by the backends.

Parameters:

ParameterTypeDescription
ideal&[SparseMultivariatePolynomial<D, O>]List of generators of the ideal
algoAlgorithmAlgorithm selection

Returns: GroebnerBasis<D, O> — a reduced Gröbner basis.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, groebner_basis, SparseMultivariatePolynomial};

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb = groebner_basis(&[f1, f2], Algorithm::Auto);
assert!(gb.is_groebner_basis());
}

See also: Algorithm, GroebnerBasis


buchberger

#![allow(unused)]
fn main() {
pub fn buchberger<D: Domain, O: MonomialOrder>(
    ideal: &[SparseMultivariatePolynomial<D, O>],
) -> GroebnerBasis<D, O>
}

Description: Convenience function — computes a Gröbner basis and then minimizes and auto-reduces it (equivalent to GroebnerBasis::buchberger(ideal).minimize().auto_reduce()).

Parameters:

ParameterTypeDescription
ideal&[SparseMultivariatePolynomial<D, O>]List of generators of the ideal

Returns: GroebnerBasis<D, O> — a reduced Gröbner basis.

See also: GroebnerBasis::buchberger, groebner_basis


f4::f4

#![allow(unused)]
fn main() {
pub fn f4<D: Domain + 'static, O: MonomialOrder>(
    ideal: &[SparseMultivariatePolynomial<D, O>],
) -> GroebnerBasis<D, O>
}

Description: Computes a Gröbner basis using the F4 matrix algorithm (Faugère 1999). Replaces sequential S-polynomial reduction with batched sparse matrix row operations (Gaussian elimination). For FiniteField coefficient domains, the native ℤ_p fast path (FpPoly i64 modular arithmetic) is used automatically, avoiding BigInt overhead.

Internal optimizations:

  • Gebauer–Moeller pair selection: chain criterion + update criterion with strict divisibility guards
  • SimpCache: caches the results of multiplying polynomials by monomials, avoiding recomputation
  • DivisorIndex: support-bitmask-based O(1)-ish reducer lookup (exact bitmasks for variable indices ≤ 63; degrades to a correctness-conservative filter above 63)
  • Two-pointer sparse row subtraction sub_scaled_fp: O(nnz) complexity
  • Native ℤ_p fast path: FpPoly uses i64 modular arithmetic, converting to/from BigInt only when reading inputs and writing outputs

Parameters:

ParameterTypeDescription
ideal&[SparseMultivariatePolynomial<D, O>]List of generators of the ideal

Returns: GroebnerBasis<D, O> — a reduced Gröbner basis.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::groebner::f4::f4;
use ocas_poly::SparseMultivariatePolynomial;

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb = f4(&[f1, f2]);
assert!(!gb.basis.is_empty());
}

See also: Algorithm::F4, f5::f5


f5::f5

#![allow(unused)]
fn main() {
pub fn f5<D: Domain + 'static, O: MonomialOrder>(
    ideal: &[SparseMultivariatePolynomial<D, O>],
) -> GroebnerBasis<D, O>
}

Description: Computes a Gröbner basis using the F5 signature algorithm (Faugère 2002). Attaches a signature (module_pos, monomial) to each polynomial and rejects zero reductions before matrix construction via the syzygy criterion, achieving order-of-magnitude speedups on hard ideals (e.g., cyclic-n).

Algorithm highlights:

  • Signatures: (module_pos, monomial) records the history of a polynomial — module_pos is the index of the input generator and monomial is the monomial multiple applied. Signatures are compared under the pot (position-over-term) order: module position first (smaller preferred), then by the monomial order O.
  • Syzygy tracking: when a matrix row reduces to zero, its signature is a syzygy. Rows whose future signatures are monomial multiples of known syzygies are skipped immediately (the F5 syzygy criterion).
  • Incremental processing: generators are processed one at a time; each new generator triggers a round of matrix construction and reduction.

The current implementation provides a general-domain F5 core. For FiniteField coefficient domains, the native ℤ_p fast path is used automatically.

Parameters:

ParameterTypeDescription
ideal&[SparseMultivariatePolynomial<D, O>]List of generators of the ideal

Returns: GroebnerBasis<D, O> — a reduced Gröbner basis (identical to the F4 output).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::groebner::f5::f5;

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb = f5(&[f1, f2]);
assert!(gb.is_groebner_basis());
}

See also: Algorithm::F5, f4::f4


fglm

#![allow(unused)]
fn main() {
pub fn fglm<D: Domain, O2: MonomialOrder>(
    gb: &GroebnerBasis<D, impl MonomialOrder>,
) -> Option<GroebnerBasis<D, O2>>
}

Description: The FGLM order-change algorithm (Faugère–Gianni–Lazard–Mora 1993). Converts a Gröbner basis of a zero-dimensional ideal from one monomial order to another, with complexity $O(n \cdot D^3)$ ($D$ is the vector space dimension of $R/I$), much faster than re-running F4 for large zero-dimensional ideals.

Algorithm steps:

  1. Compute the staircase — the set of monomials not divisible by any leading monomial
  2. Traverse the staircase monomials in the target order
  3. For each monomial, compute its normal form under the current basis
  4. If the normal form is linearly dependent on the vectors seen so far, construct a new basis element from the coefficient relation
  5. Otherwise, add it to the set of seen vectors

Parameters:

ParameterTypeDescription
gb&GroebnerBasis<D, impl MonomialOrder>The input Gröbner basis (must be reduced)

Returns: Option<GroebnerBasis<D, O2>> — Some(gb) is the converted basis; None means the ideal is not zero-dimensional (the staircase is infinite).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::{Grevlex, Lex};
use ocas_poly::{GroebnerBasis, SparseMultivariatePolynomial, f4};
use ocas_poly::groebner::fglm::fglm;

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let gb_lex = f4::f4(&[f1, f2]);
let gb_grevlex = fglm::<_, Grevlex>(&gb_lex).expect("zero-dimensional");
assert!(gb_grevlex.is_groebner_basis());
}

See also: GroebnerBasis::reorder (simple order change, works for any dimension), is_zero_dimensional


HilbertSeries

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct HilbertSeries {
    pub numerator: Vec<i64>,
    pub denominator_power: usize,
}
}

Description: The Hilbert series of the quotient ring $R/I$, represented as the rational function $H(t) = N(t) / (1-t)^n$. The numerator $N(t)$ is stored as a coefficient vector (numerator[i] is the coefficient of $t^i$), and the denominator is $(1-t)^n$ ($n$ is the number of variables).

Fields:

FieldTypeDescription
numeratorVec<i64>Coefficients of the numerator $N(t)$ (starting from the constant term)
denominator_powerusizePower of $(1-t)$ (= the number of variables $n$)

hilbert_function

#![allow(unused)]
fn main() {
pub fn hilbert_function(&self, degree: usize) -> i64
}

Description: Computes the Hilbert function value $\dim_k (R/I)_d$ at degree $d$. Uses the formula $H(d) = [t^d] N(t) / (1-t)^n$, where the coefficient of $t^k$ in $(1-t)^{-n}$ is $\binom{n+k-1}{k}$.

Parameters:

ParameterTypeDescription
degreeusizeThe degree $d$

Returns: i64 — the value of $\dim_k (R/I)_d$.

dimension

#![allow(unused)]
fn main() {
pub fn dimension(&self) -> usize
}

Description: Computes the Krull dimension of $R/I$. Obtained by checking the order of vanishing of the numerator at $t=1$ (differentiating repeatedly until the value at $t=1$ is nonzero).

Returns: usize — the Krull dimension. Zero-dimensional ideals return 0.

degree

#![allow(unused)]
fn main() {
pub fn degree(&self) -> i64
}

Description: Computes the degree of the projective variety. For a well-formed Hilbert series, this equals the value of the numerator at $t=1$ after removing the dimension factors.

Returns: i64 — the degree of the projective variety.

hilbert_polynomial

#![allow(unused)]
fn main() {
pub fn hilbert_polynomial(&self) -> Vec<f64>
}

Description: Computes the coefficients of the Hilbert polynomial $P(d)$ ($H(d) = P(d)$ for $d \gg 0$) using Lagrange interpolation. Returns the coefficients in ascending power order (result[i] is the coefficient of $d^i$); the degree of the polynomial is self.dimension().

Returns: Vec<f64> — the Hilbert polynomial coefficients (ascending power order).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, SparseMultivariatePolynomial, groebner_basis};
use ocas_poly::groebner::hilbert::hilbert_series;

let d = RationalDomain;
// Ideal (x² - 1, y² - 1): LM ideal (x², y²), numerator N(t) = 1 - 2t² + t⁴
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
    (vec![0, 0], Rational::new(-1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![0, 2], Rational::new(1, 1)),
    (vec![0, 0], Rational::new(-1, 1)),
]);
let gb = groebner_basis(&[f1, f2], Algorithm::F4);
let hs = hilbert_series(&gb);
assert_eq!(hs.dimension(), 0); // zero-dimensional: 4 solutions
assert_eq!(hs.degree(), 4); // degree = number of solutions
println!("dimension: {}, degree: {}", hs.dimension(), hs.degree());
for d in 0..10 {
    println!("H({}) = {}", d, hs.hilbert_function(d));
}
let hp = hs.hilbert_polynomial();
println!("Hilbert polynomial coefficients: {:?}", hp);
}

See also: hilbert_series


hilbert_series

#![allow(unused)]
fn main() {
pub fn hilbert_series(
    gb: &GroebnerBasis<RationalDomain, Lex>,
) -> HilbertSeries
}

Description: Computes the Hilbert series of $R/I$ from a Gröbner basis. Uses Macaulay's theorem: the Hilbert series of $R/I$ equals the Hilbert series of $R/\text{LM}(I)$ (the leading-term ideal). The Hilbert numerator of the monomial ideal $\langle m_1, \dots, m_s \rangle$ is computed via inclusion–exclusion: $N(t) = \sum_k (-1)^k \sum_{|S|=k} t^{\deg \text{lcm}(S)}$.

Parameters:

ParameterTypeDescription
gb&GroebnerBasis<RationalDomain, Lex>A Lex-order Gröbner basis over the rationals

Returns: HilbertSeries — the Hilbert series of the quotient ring.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, SparseMultivariatePolynomial, groebner_basis};
use ocas_poly::groebner::hilbert::hilbert_series;

let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 1], Rational::new(1, 1)),
]);
let gb = groebner_basis(&[f1, f2], Algorithm::F4);
let hs = hilbert_series(&gb);
assert!(hs.dimension() <= 2);
}

See also: HilbertSeries, is_zero_dimensional


eliminate

#![allow(unused)]
fn main() {
pub fn eliminate<D: Domain + 'static>(
    ideal: &[SparseMultivariatePolynomial<D, Lex>],
    elim_vars: usize,
    algo: Algorithm,
) -> GroebnerBasis<D, Lex>
}

Description: Eliminates variables from an ideal. Returns a Gröbner basis of $I \cap k[x_{\text{elim_vars}}, \dots, x_{n-1}]$, i.e., the polynomials that do not contain the first elim_vars variables. Uses the Lex order — under Lex, the reduced Gröbner basis of an ideal automatically contains generators of the elimination ideal.

Parameters:

ParameterTypeDescription
ideal&[SparseMultivariatePolynomial<D, Lex>]The generators of the ideal (must be in Lex order)
elim_varsusizeThe number of variables to eliminate (eliminates $x_0, \dots, x_{\text{elim_vars}-1}$)
algoAlgorithmGröbner basis algorithm selection

Returns: GroebnerBasis<D, Lex> — a Gröbner basis of the elimination ideal.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{SparseMultivariatePolynomial, eliminate, Algorithm};

let d = RationalDomain;
// Ideal: x + y + z, x*y + x*z in k[x,y,z]; eliminate x
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 3, vec![
    (vec![1, 0, 0], Rational::new(1, 1)),
    (vec![0, 1, 0], Rational::new(1, 1)),
    (vec![0, 0, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 3, vec![
    (vec![1, 1, 0], Rational::new(1, 1)),
    (vec![1, 0, 1], Rational::new(1, 1)),
]);
let elim = eliminate(&[f1, f2], 1, Algorithm::Auto);
// The result should lie in k[y,z]
for p in &elim.basis {
    assert!(p.degree_in(0) == 0, "the eliminated variable x should not appear");
}
}

See also: groebner_basis, ideal_quotient


Ideal operations

All ideal operations live in the ocas_poly::ideal module and use the Lex order for consistency with elimination computations.

ideal_contains

#![allow(unused)]
fn main() {
pub fn ideal_contains<D: Domain + 'static>(
    generators: &[SparseMultivariatePolynomial<D, Lex>],
    f: &SparseMultivariatePolynomial<D, Lex>,
    algo: Algorithm,
) -> bool
}

Description: Tests whether $f$ belongs to the ideal generated by the generators. Computes a Gröbner basis of the ideal and reduces $f$; $f \in I$ if and only if the remainder is zero.

Parameters:

ParameterTypeDescription
generators&[SparseMultivariatePolynomial<D, Lex>]The generators of the ideal
f&SparseMultivariatePolynomial<D, Lex>The polynomial to test
algoAlgorithmGröbner basis algorithm selection

Returns: bool — true if $f \in I$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::ideal_contains;
use ocas_poly::{Algorithm, SparseMultivariatePolynomial};

let d = RationalDomain;
let x = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
]);
let y = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![0, 1], Rational::new(1, 1)),
]);
assert!(ideal_contains(&[x.clone(), y.clone()], &x, Algorithm::Auto));
assert!(!ideal_contains(&[y], &x, Algorithm::Auto));  // x ∉ ⟨y⟩
}

See also: groebner_basis


ideal_sum

#![allow(unused)]
fn main() {
pub fn ideal_sum<D: Domain + 'static>(
    generators_a: &[SparseMultivariatePolynomial<D, Lex>],
    generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Description: The sum of two ideals $I + J = \langle f_1, \dots, f_m, g_1, \dots, g_n \rangle$. Merges the generators and computes a Gröbner basis.

Parameters:

ParameterTypeDescription
generators_a&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $I$
generators_b&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $J$

Returns: GroebnerBasis<D, Lex> — a reduced Gröbner basis of $I + J$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::ideal_sum;
use ocas_poly::{Algorithm, SparseMultivariatePolynomial};

let d = RationalDomain;
let x = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
]);
let y = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![0, 1], Rational::new(1, 1)),
]);
let gb = ideal_sum(&[x], &[y]);
// ⟨x⟩ + ⟨y⟩ = ⟨x, y⟩
assert!(gb.basis.len() >= 2);
}

See also: ideal_intersection


ideal_product

#![allow(unused)]
fn main() {
pub fn ideal_product<D: Domain + 'static>(
    generators_a: &[SparseMultivariatePolynomial<D, Lex>],
    generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Description: The product of two ideals $I \cdot J = \langle f_i \cdot g_j \rangle$. Computes all products $f_i g_j$ and then a Gröbner basis.

Parameters:

ParameterTypeDescription
generators_a&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $I$
generators_b&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $J$

Returns: GroebnerBasis<D, Lex> — a reduced Gröbner basis of $I \cdot J$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::ideal_product;
use ocas_poly::SparseMultivariatePolynomial;

let d = RationalDomain;
let x = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
]);
let y = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![0, 1], Rational::new(1, 1)),
]);
let gb = ideal_product(&[x], &[y]);
// ⟨x⟩ · ⟨y⟩ = ⟨xy⟩
assert_eq!(gb.basis.len(), 1);
}

See also: ideal_quotient


ideal_quotient

#![allow(unused)]
fn main() {
pub fn ideal_quotient<D: Domain + 'static>(
    generators_i: &[SparseMultivariatePolynomial<D, Lex>],
    generators_j: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Description: The quotient of ideals $I : J = {f : f \cdot g \in I, \forall g \in J}$. Uses the Rabinowitsch trick for each generator $g$ of $J$: compute $\text{GB}(I \cup {1 - w \cdot g})$ in the extended ring $k[x_1, \dots, x_n, w]$, eliminate $w$, and intersect the results.

Parameters:

ParameterTypeDescription
generators_i&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $I$
generators_j&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $J$

Returns: GroebnerBasis<D, Lex> — a reduced Gröbner basis of $I : J$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::ideal_quotient;
use ocas_poly::SparseMultivariatePolynomial;

let d = RationalDomain;
// ⟨x², xy⟩ : ⟨x⟩ = ⟨x⟩
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 1], Rational::new(1, 1)),
]);
let g = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
]);
let gb = ideal_quotient(&[f1, f2], &[g]);
assert!(!gb.basis.is_empty());
}

See also: ideal_saturate, ideal_contains


ideal_saturate

#![allow(unused)]
fn main() {
pub fn ideal_saturate<D: Domain + 'static>(
    generators_i: &[SparseMultivariatePolynomial<D, Lex>],
    generators_j: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Description: The saturation of an ideal $I : J^\infty = \bigcup_k (I : J^k)$. Iteratively computes $I : J$, $(I : J) : J$, etc., until stabilization.

Parameters:

ParameterTypeDescription
generators_i&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $I$
generators_j&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $J$

Returns: GroebnerBasis<D, Lex> — a reduced Gröbner basis of $I : J^\infty$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::ideal_saturate;
use ocas_poly::SparseMultivariatePolynomial;

let d = RationalDomain;
// ⟨x²y, xy²⟩ : ⟨x⟩^∞ = ⟨y⟩
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 1], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 2], Rational::new(1, 1)),
]);
let g = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
]);
let gb = ideal_saturate(&[f1, f2], &[g]);
assert!(!gb.basis.is_empty());
}

See also: ideal_quotient


ideal_intersection

#![allow(unused)]
fn main() {
pub fn ideal_intersection<D: Domain + 'static>(
    generators_a: &[SparseMultivariatePolynomial<D, Lex>],
    generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Description: The intersection of two ideals $I \cap J$. Uses an auxiliary variable $t$: $I \cap J = \langle t \cdot f_i, (1-t) \cdot g_j \rangle \cap k[x_1, \dots, x_n]$.

Parameters:

ParameterTypeDescription
generators_a&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $I$
generators_b&[SparseMultivariatePolynomial<D, Lex>]The generators of ideal $J$

Returns: GroebnerBasis<D, Lex> — a reduced Gröbner basis of $I \cap J$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::ideal_intersection;
use ocas_poly::SparseMultivariatePolynomial;

let d = RationalDomain;
let x = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
]);
let y = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![0, 1], Rational::new(1, 1)),
]);
let gb = ideal_intersection(&[x], &[y]);
// ⟨x⟩ ∩ ⟨y⟩ = ⟨xy⟩
assert_eq!(gb.basis.len(), 1);
}

See also: ideal_sum, eliminate


ideal_radical

#![allow(unused)]
fn main() {
pub fn ideal_radical(
    generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> GroebnerBasis<RationalDomain, Lex>
}

Description: Computes the radical $\sqrt{I}$ of the ideal.

  • Zero-dimensional ideals: computed via the square-free factorization of the univariate polynomials in the Lex GB (the generators of $\sqrt{I}$ consist of the square-free univariate polynomials for each variable together with the non-univariate basis elements).
  • Positive-dimensional ideals: uses the Jacobian saturation method (a simplified Kemper algorithm): $\sqrt{I} = I : h^\infty$. The current implementation picks $h$ heuristically — the non-trivial partial derivative of smallest total degree (not the exact GCD). If all partial derivatives are constants or zero (trivial Jacobian), the original GB is returned conservatively.

Parameters:

ParameterTypeDescription
generators&[SparseMultivariatePolynomial<RationalDomain, Lex>]The generators of the ideal

Returns: GroebnerBasis<RationalDomain, Lex> — a reduced Gröbner basis of $\sqrt{I}$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::ideal_radical;
use ocas_poly::SparseMultivariatePolynomial;

let d = RationalDomain;
// √(x², xy) = (x)
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 1], Rational::new(1, 1)),
]);
let rad = ideal_radical(&[f1, f2]);
assert!(!rad.basis.is_empty());
}

See also: primary_decomposition


is_zero_dimensional

#![allow(unused)]
fn main() {
pub fn is_zero_dimensional(gb: &GroebnerBasis<RationalDomain, Lex>) -> bool
}

Description: Checks whether the ideal is zero-dimensional. The ideal is zero-dimensional if and only if for every variable $x_i$ some leading monomial in the GB is a pure power $x_i^N$ (equivalently, the staircase/set of standard monomials is finite).

Parameters:

ParameterTypeDescription
gb&GroebnerBasis<RationalDomain, Lex>A Lex-order Gröbner basis over the rationals

Returns: bool — true if zero-dimensional.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, GroebnerBasis, SparseMultivariatePolynomial, groebner_basis};
use ocas_poly::ideal::is_zero_dimensional;

let d = RationalDomain;
// x² - 1, y - x → zero-dimensional
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
    (vec![0, 0], Rational::new(-1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![0, 1], Rational::new(1, 1)),
    (vec![1, 0], Rational::new(-1, 1)),
]);
let gb = groebner_basis(&[f1, f2], Algorithm::F4);
assert!(is_zero_dimensional(&gb));
}

See also: solve_polynomial_system, fglm


solve_polynomial_system

#![allow(unused)]
fn main() {
pub fn solve_polynomial_system(
    equations: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
    algo: Algorithm,
) -> PolynomialSystemSolution
}

Description: Solves a zero-dimensional polynomial system. Converts the GB to Lex order, extracts the univariate polynomial for each variable, and solves by back substitution. Returns the PolynomialSystemSolution enum, distinguishing zero-dimensional (finitely many solutions), positive-dimensional (infinite solution set), and empty ($\langle 1 \rangle$).

Solution procedure:

  1. Compute a Gröbner basis
  2. Check whether it is $\langle 1 \rangle$ (empty set)
  3. Check whether it is zero-dimensional
  4. Convert to a triangular decomposition in Lex order
  5. Back-substitute starting from the last variable: for each variable, find the real roots of the univariate polynomial (Sturm's theorem isolation + refinement to $10^{-14}$), recursively substituting the known values

Parameters:

ParameterTypeDescription
equations&[SparseMultivariatePolynomial<RationalDomain, Lex>]The system of equations (polynomials = 0)
algoAlgorithmGröbner basis algorithm selection

Returns: PolynomialSystemSolution — the solution result.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::{Algorithm, SparseMultivariatePolynomial, groebner_basis};
use ocas_poly::ideal::{solve_polynomial_system, PolynomialSystemSolution};

let d = RationalDomain;
// x² + y² - 1, x - y → solutions at (±1/√2, ±1/√2)
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
    (vec![0, 2], Rational::new(1, 1)),
    (vec![0, 0], Rational::new(-1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);
let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
match sol {
    PolynomialSystemSolution::ZeroDimensional(z) => {
        assert_eq!(z.solutions.len(), 2);
    }
    _ => panic!("expected zero-dimensional"),
}
}

See also: PolynomialSystemSolution, is_zero_dimensional


primary_decomposition

#![allow(unused)]
fn main() {
pub fn primary_decomposition(
    generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> Vec<PrimaryComponent>
}

Description: Computes the primary decomposition of the ideal.

  • Zero-dimensional ideals: factor the univariate polynomial in the first variable of the Lex GB and separate the primary components by saturation (for each factor $f_i$, compute $I : (\prod_{j \neq i} f_j)^\infty$).
  • Positive-dimensional ideals: conservatively return a single primary component (the original GB itself as both primary and prime).

Parameters:

ParameterTypeDescription
generators&[SparseMultivariatePolynomial<RationalDomain, Lex>]The generators of the ideal

Returns: Vec<PrimaryComponent> — the list of primary components.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::sparse::Lex;
use ocas_poly::ideal::primary_decomposition;
use ocas_poly::SparseMultivariatePolynomial;

let d = RationalDomain;
// (x², xy) is positive-dimensional → one conservative component
// (ideally (x², xy) = (x) ∩ (x², y))
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
]);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 1], Rational::new(1, 1)),
]);
let decomp = primary_decomposition(&[f1, f2]);
assert!(decomp.len() >= 1);
}

See also: PrimaryComponent, ideal_radical


is_prime_ideal

#![allow(unused)]
fn main() {
pub fn is_prime_ideal(
    generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> bool
}

Description: Tests whether the ideal is prime.

  • Zero-dimensional ideals: checks whether the univariate polynomials in the Lex GB are irreducible (for polynomials of degree ≤ 3, the rational root theorem is used).
  • Positive-dimensional ideals: conservatively returns false (a complete implementation would require checking the irreducibility of the variety, which is not yet implemented).

Note: This is a conservative approximation — it never returns a false positive (a non-prime ideal reported as prime); it can only return a false negative (a prime ideal reported as non-prime).

Parameters:

ParameterTypeDescription
generators&[SparseMultivariatePolynomial<RationalDomain, Lex>]The generators of the ideal

Returns: bool — true for a (confirmed) prime ideal.

See also: is_primary_ideal, primary_decomposition


is_primary_ideal

#![allow(unused)]
fn main() {
pub fn is_primary_ideal(
    generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> bool
}

Description: Tests whether the ideal is primary. A primary ideal has exactly one associated prime ideal, i.e., primary_decomposition returns at most one component.

Parameters:

ParameterTypeDescription
generators&[SparseMultivariatePolynomial<RationalDomain, Lex>]The generators of the ideal

Returns: bool — true if the ideal is primary.

See also: primary_decomposition, is_prime_ideal


PrimaryComponent

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct PrimaryComponent {
    pub primary: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
    pub prime: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
}
}

Description: A primary component of an ideal — a primary ideal together with its associated prime ideal (i.e., the radical of the primary ideal).

Fields:

FieldTypeDescription
primaryVec<SparseMultivariatePolynomial<RationalDomain, Lex>>The generators of the primary ideal
primeVec<SparseMultivariatePolynomial<RationalDomain, Lex>>The generators of the associated prime ideal (the radical)

See also: primary_decomposition


PolynomialSystemSolution

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub enum PolynomialSystemSolution {
    ZeroDimensional(ZeroDimSolutions),
    PositiveDimensional(GroebnerBasis<RationalDomain, Lex>),
    Empty,
}
}

Description: The result of solving a polynomial system, classified by the type of the solution set.

Variants:

VariantDescription
ZeroDimensional(ZeroDimSolutions)Finitely many real solutions (zero-dimensional ideal)
PositiveDimensional(GroebnerBasis<RationalDomain, Lex>)Infinite solution set (positive-dimensional ideal); returns a Lex-order Gröbner basis
EmptyNo solutions (the ideal is $\langle 1 \rangle$)

See also: solve_polynomial_system

ZeroDimSolutions

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct ZeroDimSolutions {
    pub solutions: Vec<RealSolution>,
    pub vector_space_dimension: usize,
}
}

Fields:

FieldTypeDescription
solutionsVec<RealSolution>The list of real solutions found
vector_space_dimensionusizeThe vector space dimension of the quotient ring $k[x_1,\dots,x_n]/I$ (the total number of solutions over $\mathbb{C}$, counted with multiplicity)

RealSolution

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct RealSolution {
    pub values: Vec<f64>,
    pub multiplicity: usize,
}
}

Fields:

FieldTypeDescription
valuesVec<f64>The value of each variable (one per variable)
multiplicityusizeThe algebraic multiplicity of the solution

Module path quick reference

Function/TypeFull path
Algorithmocas_poly::Algorithm
GroebnerBasisocas_poly::GroebnerBasis
groebner_basisocas_poly::groebner_basis
buchbergerocas_poly::buchberger
eliminateocas_poly::eliminate
f4::f4ocas_poly::groebner::f4::f4
f5::f5ocas_poly::groebner::f5::f5
fglmocas_poly::groebner::fglm::fglm
HilbertSeriesocas_poly::groebner::hilbert::HilbertSeries
hilbert_seriesocas_poly::groebner::hilbert::hilbert_series
ideal_containsocas_poly::ideal::ideal_contains
ideal_sumocas_poly::ideal::ideal_sum
ideal_productocas_poly::ideal::ideal_product
ideal_quotientocas_poly::ideal::ideal_quotient
ideal_saturateocas_poly::ideal::ideal_saturate
ideal_intersectionocas_poly::ideal::ideal_intersection
ideal_radicalocas_poly::ideal::ideal_radical
primary_decompositionocas_poly::ideal::primary_decomposition
is_zero_dimensionalocas_poly::ideal::is_zero_dimensional
solve_polynomial_systemocas_poly::ideal::solve_polynomial_system
is_prime_idealocas_poly::ideal::is_prime_ideal
is_primary_idealocas_poly::ideal::is_primary_ideal
PrimaryComponentocas_poly::ideal::PrimaryComponent
PolynomialSystemSolutionocas_poly::ideal::PolynomialSystemSolution
ZeroDimSolutionsocas_poly::ideal::ZeroDimSolutions
RealSolutionocas_poly::ideal::RealSolution

Factorization

This chapter documents the complete polynomial factorization API in oCAS, covering factorization over the univariate rings $\mathbb{Z}[x]$ and $\mathbb{F}_p[x]$, the multivariate rings $\mathbb{Z}[x_1,\dots,x_n]$ and $\mathbb{F}_p[x_1,\dots,x_n]$, and algebraic number fields $\mathbb{Q}(\alpha)[x]$, as well as rational function arithmetic and resultants.

Related modules:

Module pathDescription
ocas_poly::factorTop-level factorization entry point
ocas_poly::factor::henselHensel lifting and Zassenhaus recombination ($\mathbb{Z}[x]$)
ocas_poly::factor::finite_field$\mathbb{F}_p[x]$ factorization (Cantor–Zassenhaus + Berlekamp)
ocas_poly::factor::multivariateBivariate Hensel lifting (Wang's algorithm)
ocas_poly::factor::eezMultivariate EEZ Hensel lifting (Wang + leading-coefficient reconstruction)
ocas_poly::factor::algebraicAlgebraic number field factorization (Trager's algorithm)
ocas_poly::gcdGCD (pseudo-remainder + Euclidean algorithm)
ocas_poly::gcd::modularModular GCD (Brown 1971)
ocas_poly::resultantResultant (Brown PRS)
ocas_poly::rationalRational function fraction field
ocas_calc::partial_fractionPartial fraction decomposition

Type Aliases

#![allow(unused)]
fn main() {
/// Square-free factorization result: list of (factor, multiplicity) pairs.
pub type SquareFreeFactors<D> = Vec<(DenseUnivariatePolynomial<D>, usize)>;

/// Complete factorization result: list of (irreducible factor, multiplicity) pairs.
pub type Factors<D> = Vec<(DenseUnivariatePolynomial<D>, usize)>;
}

1. Univariate Integer Polynomial Factorization ($\mathbb{Z}[x]$)

DenseUnivariatePolynomial::factor

Signature: pub fn factor(&self) -> Factors<IntegerDomain>

Description: Factors a univariate integer polynomial into irreducible factors (primitive polynomials that are irreducible over $\mathbb{Q}$) together with their multiplicities.

Parameters: none (self is the polynomial to factor).

Returns: Vec<(DenseUnivariatePolynomial<IntegerDomain>, usize)> — a list of irreducible factor and multiplicity pairs. Each factor is a primitive polynomial with a positive leading coefficient. The product of all factors (each raised to its multiplicity) equals primitive_part(self); the input must be primitive (content = 1), otherwise the content is discarded.

Algorithm:

  1. The input must be primitive (content = 1; preprocess with primitive_part() if needed).
  2. Square-free factorization (Yun's algorithm): use $\gcd(f, f')$ to separate multiplicities.
  3. Call factor_square_free on each square-free component (internally it handles non-monic inputs via the leading-coefficient transformation, then calls factor_square_free_monic):
    • Choose a prime $p$ ($p \nmid \mathrm{lc}(f)$ and $f \bmod p$ square-free).
    • Factor into monic irreducible factors in $\mathbb{F}_p[x]$ (Cantor–Zassenhaus).
    • Compute the Mignotte bound $B = 2^n |f|_2$.
    • Linear Hensel lifting $p \to p^k$ ($p^k > 2B$).
    • Zassenhaus subset recombination: enumerate subsets of the lifted factors and verify by trial division.
  4. Non-monic polynomials are handled via the leading-coefficient transformation $a^{d-1} f(x/a)$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// x^2 - 1 = (x-1)(x+1)
let p = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(0), Integer::from(1),
]);
let factors = p.factor();
assert_eq!(factors.len(), 2);
// Each factor has degree 1 and multiplicity 1
for (g, m) in &factors {
    assert_eq!(g.degree(), Some(1));
    assert_eq!(*m, 1);
}
// Output: [(x - 1, 1), (x + 1, 1)]
}

See also: square_free_factorization, factor_over_finite_field, Polynomial GCD and Factorization


DenseUnivariatePolynomial::square_free_factorization

Signature: pub fn square_free_factorization(&self) -> SquareFreeFactors<D>

Description: Computes the square-free factorization of the polynomial. Returns the distinct square-free factors together with their multiplicities in the original polynomial.

Parameters: none (self is the polynomial to factor).

Returns: Vec<(DenseUnivariatePolynomial<D>, usize)> — a list of square-free factor and multiplicity pairs.

Algorithm: Yun's algorithm (characteristic 0). Let $g = \gcd(f, f')$, $w = f/g$, then iterate $h = \gcd(w, g)$, $z = w/h$ to collect the factors of multiplicity $k$.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// (x+1)^2 * (x-1) = x^3 + x^2 - x - 1
let p = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(-1), Integer::from(1), Integer::from(1),
]);
let factors = p.square_free_factorization();
assert_eq!(factors.len(), 2);
// Output: [(x - 1, 1), (x + 1, 2)] (Yun's algorithm collects the multiplicity-1 factor first)
}

See also: is_square_free, factor


DenseUnivariatePolynomial::is_square_free

Signature: pub fn is_square_free(&self) -> bool

Description: Tests whether the polynomial is square-free (i.e., $\gcd(f, f') = 1$).

Parameters: none.

Returns: bool — true if the polynomial is square-free.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// x^2 - 1 is square-free
let p = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(0), Integer::from(1),
]);
assert!(p.is_square_free());

// (x+1)^2 is not square-free
let q = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(1), Integer::from(2), Integer::from(1),
]);
assert!(!q.is_square_free());
}

2. Univariate Finite Field Polynomial Factorization ($\mathbb{F}_p[x]$)

factor_over_finite_field

Signature: pub fn factor_over_finite_field(f: &FpPoly) -> Vec<(FpPoly, usize)>

Description: Factors a polynomial in $\mathbb{F}_p[x]$ into monic irreducible factors with multiplicities.

Parameters:

ParameterTypeDescription
f&DenseUnivariatePolynomial<FiniteField>The polynomial to factor

Returns: Vec<(FpPoly, usize)> — a list of monic irreducible factor and multiplicity pairs. The leading coefficient is listed separately as a constant factor (multiplicity 1).

Algorithm:

  1. Square-free factorization (Musser/Bernardin algorithm, handling $p$-th roots in characteristic $p$).
  2. For each square-free component:
    • DDF (distinct-degree factorization): groups factors by degree using the Frobenius map $x \mapsto x^{p^d} \bmod f$.
    • EDF (equal-degree factorization): for odd characteristic $p$, pick a random $a$ and compute $\gcd(f, a^{(p^d-1)/2} - 1)$; for characteristic 2, use the trace map.
  3. For small primes, Berlekamp's algorithm (the kernel of the Frobenius matrix $Q^T - I$) can also be used.

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::{Domain, FiniteField};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_poly::factor::finite_field::factor_over_finite_field;

let f = FiniteField::new(BigInt::from(5));
// x^2 - 1 = (x-1)(x+1) over F_5
let p = DenseUnivariatePolynomial::from_coeffs(
    f.clone(), vec![f.element(4), f.element(0), f.element(1)]);
let factors = factor_over_finite_field(&p);
let linear_count = factors.iter()
    .filter(|(g, _)| g.degree() == Some(1)).count();
assert_eq!(linear_count, 2);
// Output: two linear monic factors
}

See also: Cantor–Zassenhaus algorithm details


DenseUnivariatePolynomial::factor (FiniteField)

Signature: pub fn factor(&self) -> Factors<FiniteField>

Description: The factor() method on DenseUnivariatePolynomial<FiniteField>, equivalent to calling factor_over_finite_field.

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::{Domain, FiniteField};
use ocas_poly::DenseUnivariatePolynomial;

let f = FiniteField::new(BigInt::from(5));
// x^2 - 1 over F_5
let p = DenseUnivariatePolynomial::from_coeffs(
    f.clone(), vec![f.element(4), f.element(0), f.element(1)]);
let factors = p.factor();
assert!(!factors.is_empty());
}

berlekamp

Signature: pub fn berlekamp(f: &FpPoly) -> Vec<FpPoly>

Description: Berlekamp's factorization algorithm, suitable for small prime fields. Builds the Frobenius matrix $Q$ (where $Q[i][j]$ is the coefficient of $x^j$ in $x^{ip} \bmod f$) and computes the kernel of $Q^T - I$; every nonzero kernel vector $v$ satisfies $v^p \equiv v \pmod{f}$ and splits factors via $\gcd(f, v - a)$ for $a \in \mathbb{F}_p$.

Parameters:

ParameterTypeDescription
f&FpPolyA monic square-free polynomial

Returns: Vec<FpPoly> — a list of monic irreducible factors.

See also: Berlekamp (1970)


cantor_zassenhaus

Signature: pub fn cantor_zassenhaus(f: &FpPoly) -> Vec<FpPoly>

Description: Cantor–Zassenhaus factorization algorithm. Runs DDF (distinct-degree factorization) first, then EDF (equal-degree factorization). Returns the list of monic irreducible factors of a monic square-free polynomial.

Parameters:

ParameterTypeDescription
f&FpPolyA monic square-free polynomial

Returns: Vec<FpPoly> — a list of monic irreducible factors.

See also: Cantor & Zassenhaus (1981)


poly_pow_mod

Signature: pub fn poly_pow_mod(base: &FpPoly, exp: &BigInt, modulus: &FpPoly) -> FpPoly

Description: Computes $\text{base}^{\text{exp}} \bmod \text{modulus}$ using fast exponentiation (repeated squaring), reducing after each multiplication.

Parameters:

ParameterTypeDescription
base&FpPolyThe base polynomial
exp&BigIntThe exponent (non-negative)
modulus&FpPolyThe modulus polynomial (nonzero)

Returns: FpPoly — the result of the modular exponentiation.

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::{Domain, FiniteField};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_poly::factor::finite_field::poly_pow_mod;

let f = FiniteField::new(BigInt::from(7));
let m = DenseUnivariatePolynomial::from_coeffs(
    f.clone(), vec![f.element(1), f.element(0), f.element(1)]); // x^2 + 1
let base = DenseUnivariatePolynomial::from_coeffs(
    f.clone(), vec![f.element(0), f.element(1)]); // x
// x^2 mod (x^2+1) = -1 = 6 in F_7
let r = poly_pow_mod(&base, &BigInt::from(2), &m);
assert_eq!(r.coeff(0).cloned(), Some(f.element(6)));
// Output: 6 (i.e., -1 mod 7)
}

3. Multivariate Integer Polynomial Factorization ($\mathbb{Z}[x_1,\dots,x_n]$)

SparseMultivariatePolynomial::factor (IntegerDomain, Lex)

Signature: pub fn factor(&self) -> Vec<(Self, usize)>

Description: Factors a sparse multivariate integer polynomial into irreducible factors with multiplicities.

Parameters: none (self is the SparseMultivariatePolynomial<IntegerDomain, Lex> to factor).

Returns: Vec<(SparseMultivariatePolynomial<IntegerDomain, Lex>, usize)> — a list of irreducible factor and multiplicity pairs.

Algorithm selection:

  • $n \geq 3$ variables: uses EEZ Hensel lifting (Wang leading-coefficient reconstruction + p-adic coefficient Hensel lifting + Zassenhaus recombination).
  • Bivariate with constant leading coefficient: uses bivariate Hensel lifting (evaluation–lifting path).
  • Bivariate with non-constant leading coefficient: falls back to the EEZ path.

Algorithm (EEZ path, $n \geq 3$):

  1. Extract the content in the main variable and compute the square-free factorization.
  2. Pick sample points $(a_1, \dots, a_n)$ such that the univariate image $f(x_0, a_1, \dots)$ keeps its degree and is square-free.
  3. Factor the univariate image over $\mathbb{Z}$.
  4. Wang leading-coefficient reconstruction: distribute the irreducible factors of the total leading coefficient among the univariate factors.
  5. EEZ variable-by-variable Hensel lifting: recover $x_1, x_2, \dots$ in turn, solving a multivariate Diophantine equation at each step.
  6. p-adic coefficient Hensel lifting: lift from modulus $p$ to a sufficiently large $p^k$.
  7. Zassenhaus subset recombination: enumerate subsets of the modular factors and verify by trial division.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::Lex;

// (x^2 + y + 1)(x + y + 2) in Z[x,y]
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
    IntegerDomain, 2,
    vec![
        (vec![3, 0], Integer::from(1)),   // x^3
        (vec![2, 1], Integer::from(1)),   // x^2 y
        (vec![2, 0], Integer::from(2)),   // 2x^2
        (vec![1, 1], Integer::from(1)),   // xy
        (vec![1, 0], Integer::from(1)),   // x
        (vec![0, 2], Integer::from(1)),   // y^2
        (vec![0, 1], Integer::from(3)),   // 3y
        (vec![0, 0], Integer::from(2)),   // 2
    ],
);
let factors = f.factor();
assert!(factors.len() >= 2);
// Output: [(x^2 + y + 1, 1), (x + y + 2, 1)]
}

See also: Wang EEZ algorithm, bivariate_factor_z


bivariate_factor_z

Signature: pub fn bivariate_factor_z(f: &ZMPoly, x_var: usize, y_var: usize) -> Vec<(ZMPoly, usize)>

Description: Factors a bivariate integer polynomial into irreducible factors with multiplicities. Requires the leading coefficient in $x$ to be a nonzero integer constant.

Parameters:

ParameterTypeDescription
f&SparseMultivariatePolynomial<IntegerDomain, Lex>The polynomial to factor
x_varusizeIndex of the main variable
y_varusizeIndex of the secondary variable

Returns: Vec<(ZMPoly, usize)> — irreducible factor and multiplicity pairs.

Algorithm:

  1. Bivariate square-free factorization (heuristic bivariate GCD).
  2. Choose $y = \alpha$ so that the univariate image $f(x, \alpha)$ is square-free with the fewest factors.
  3. Hensel lifting: lift the factors of $f(x, \alpha)$ back to bivariate factors, correcting iteratively via Taylor expansion.

See also: Wang (1978)


multivariate_factor_z

Signature: pub fn multivariate_factor_z(f: &ZmPoly) -> Vec<(ZmPoly, usize)>

Description: Entry point for multivariate factorization via EEZ Hensel lifting. Supports non-constant leading coefficients (through Wang leading-coefficient reconstruction).

Parameters:

ParameterTypeDescription
f&SparseMultivariatePolynomial<IntegerDomain, Lex>The polynomial to factor

Returns: Vec<(ZmPoly, usize)> — irreducible factor and multiplicity pairs.

See also: Wang EEZ algorithm details


4. Multivariate Finite Field Polynomial Factorization ($\mathbb{F}_p[x_1,\dots,x_n]$)

SparseMultivariatePolynomial::factor (FiniteField, Lex)

Signature: pub fn factor(&self) -> Vec<(Self, usize)>

Description: Factors a sparse multivariate finite field polynomial into irreducible factors with multiplicities.

Parameters: none.

Returns: Vec<(SparseMultivariatePolynomial<FiniteField, Lex>, usize)> — a list of irreducible factor and multiplicity pairs.

Algorithm selection:

  • $n \geq 3$ variables: EEZ Hensel lifting.
  • Bivariate: evaluation–Hensel path.

Example:

#![allow(unused)]
fn main() {
use num_bigint::BigInt;
use ocas_domain::{Domain, FiniteField};
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::Lex;

let fp = FiniteField::new(BigInt::from(7));
// x*y + 1 over F_7[x,y] — already irreducible
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
    fp.clone(), 2,
    vec![
        (vec![1, 1], fp.element(1)),  // xy
        (vec![0, 0], fp.element(1)),  // 1
    ],
);
let factors = f.factor();
assert_eq!(factors.len(), 1);
assert_eq!(factors[0].1, 1); // multiplicity 1
}

bivariate_factor_fp

Signature: pub fn bivariate_factor_fp(f: &FpMPoly, x_var: usize, y_var: usize) -> Vec<(FpMPoly, usize)>

Description: Factors a bivariate $\mathbb{F}_p$ polynomial into irreducible factors with multiplicities.

Parameters:

ParameterTypeDescription
f&SparseMultivariatePolynomial<FiniteField, Lex>The polynomial to factor
x_varusizeIndex of the main variable
y_varusizeIndex of the secondary variable

Returns: Vec<(FpMPoly, usize)> — irreducible factor and multiplicity pairs.


multivariate_factor_fp

Signature: pub fn multivariate_factor_fp(f: &FpMPoly) -> Vec<(FpMPoly, usize)>

Description: Entry point for multivariate $\mathbb{F}_p$ factorization, supporting non-constant leading coefficients (Wang leading-coefficient reconstruction + EEZ lifting).

Parameters:

ParameterTypeDescription
f&SparseMultivariatePolynomial<FiniteField, Lex>The polynomial to factor

Returns: Vec<(FpMPoly, usize)> — irreducible factor and multiplicity pairs.


5. Algebraic Number Field Factorization ($\mathbb{Q}(\alpha)[x]$)

DenseUnivariatePolynomial::factor (AlgebraicNumberField)

Signature: pub fn factor(&self) -> Factors<AlgebraicNumberField>

Description: Factors a polynomial over the algebraic number field $\mathbb{Q}(\alpha)$ into monic irreducible factors with multiplicities (Trager's algorithm).

Parameters: none (self is a DenseUnivariatePolynomial<AlgebraicNumberField>).

Returns: Vec<(DenseUnivariatePolynomial<AlgebraicNumberField>, usize)> — monic irreducible factor and multiplicity pairs. The product of all factors (each raised to its multiplicity) equals self divided by its leading coefficient (a unit of $K$).

Algorithm (Trager):

  1. Square-free factorization (Yun's algorithm, using the modular GCD gcd_anf to avoid coefficient blow-up).
  2. Call factor_square_free_anf on each square-free component:
    • Trager shift: find $s \geq 0$ such that the norm of $f(x - s\alpha)$ is square-free.
    • Norm computation: $\operatorname{Res}_\alpha(m(\alpha), f(x, \alpha))$, via evaluation–interpolation.
    • Factor the norm over $\mathbb{Q}$: using the Hensel path.
    • Recover the factors over $K$: compute $\gcd_K(f, g_i(\alpha))$ for each norm factor $g_i$.
  3. GCD uses the modular method (Encarnación): map to $\mathrm{GF}(p^d)$, combine via CRT, rational reconstruction, and trial-division verification.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{AlgebraicNumberField, Domain, Rational, RationalDomain};
use ocas_poly::DenseUnivariatePolynomial;

// Construct Q(√2) with minimal polynomial x^2 - 2
let field = AlgebraicNumberField::new(
    RationalDomain,
    vec![Rational::new(-2, 1), Rational::new(0, 1), Rational::new(1, 1)],
);
// x^2 - 2 factors over Q(√2) as (x - √2)(x + √2)
let f = DenseUnivariatePolynomial::from_coeffs(
    field.clone(),
    vec![
        field.from_base(Rational::new(-2, 1)),
        field.zero(),
        field.one(),
    ],
);
let factors = f.factor();
assert_eq!(factors.len(), 2);
assert!(factors.iter().all(|(g, m)| *m == 1 && g.degree() == Some(1)));
// Output: [(x - √2, 1), (x + √2, 1)]
}

See also: Algebraic Number Fields and Galois Theory, Trager's algorithm


norm_with_shift

Signature: pub(crate) fn norm_with_shift(field: &AlgebraicNumberField, f: &UP<AlgebraicNumberField>) -> Option<(u64, UP<AlgebraicNumberField>, UP<RationalDomain>)>

Description: Trager shift: find $s \geq 0$ such that the norm of $f(x - s\alpha)$ is square-free over $\mathbb{Q}$. Returns (s, g, norm). Tries at most MAX_TRAGER_SHIFTS (100) shifts.

Parameters:

ParameterTypeDescription
field&AlgebraicNumberFieldThe algebraic number field $\mathbb{Q}(\alpha)$
f&DenseUnivariatePolynomial<AlgebraicNumberField>The polynomial to factor

Returns: Option<(u64, UP<AlgebraicNumberField>, UP<RationalDomain>)> — (s, f(x - s\alpha), \operatorname{Norm}(f(x - s\alpha))).


gcd_anf

Signature: pub(crate) fn gcd_anf(field: &AlgebraicNumberField, a: &UP<AlgebraicNumberField>, b: &UP<AlgebraicNumberField>) -> UP<AlgebraicNumberField>

Description: GCD of two univariate polynomials over an algebraic number field. Uses the modular method (Encarnación): map to $\mathrm{GF}(p^d)$, combine the monic modular GCDs (CRT), rationally reconstruct the coefficients, and verify by trial division. Uses at most MAX_ANF_GCD_PRIMES (1000) primes, falling back to a dense Euclidean GCD beyond that.

Parameters:

ParameterTypeDescription
field&AlgebraicNumberFieldThe algebraic number field
a&UP<AlgebraicNumberField>The first polynomial
b&UP<AlgebraicNumberField>The second polynomial

Returns: a monic GCD polynomial.


6. Polynomial GCD

DenseUnivariatePolynomial::gcd

Signature: pub fn gcd(&self, other: &Self) -> Self

Description: Computes the greatest common divisor of two univariate polynomials. Uses the pseudo-remainder Euclidean algorithm for non-field coefficients. The result is a primitive polynomial.

Parameters:

ParameterTypeDescription
other&SelfThe other polynomial

Returns: a primitive GCD polynomial.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// x^2 - 1 = (x-1)(x+1)
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(0), Integer::from(1),
]);
// x^2 + 2x + 1 = (x+1)^2
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(1), Integer::from(2), Integer::from(1),
]);
let g = a.gcd(&b);
assert_eq!(g.coeffs(), &[Integer::from(1), Integer::from(1)]);
// Output: x + 1
}

gcd_modular_z

Signature: pub fn gcd_modular_z(a: &ZPoly, b: &ZPoly) -> ZPoly

Description: Brown's modular GCD algorithm. Computes monic GCDs in $\mathbb{F}_p[x]$ for several primes $p$, combines them via CRT into a primitive GCD in $\mathbb{Z}[x]$, and verifies by trial division. It is much more efficient than pseudo-remainder GCD for large coefficients.

Parameters:

ParameterTypeDescription
a&DenseUnivariatePolynomial<IntegerDomain>The first polynomial
b&DenseUnivariatePolynomial<IntegerDomain>The second polynomial

Returns: a primitive GCD polynomial.

Algorithm details:

  1. Choose primes $p > 2^{30}$ (to avoid small-prime issues).
  2. Compute the monic GCD in $\mathbb{F}_p[x]$, scaled by $\gamma = \gcd(\mathrm{lc}(a), \mathrm{lc}(b))$.
  3. Combine via CRT (symmetric representatives), discarding "unlucky primes" (where the GCD degree is higher than the true value).
  4. Verify by exact trial division. Tries at most MAX_PRIMES (10000) primes, falling back to pseudo-remainder GCD beyond that.

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_poly::gcd::modular::gcd_modular_z;

let d = IntegerDomain;
let i = |v: i64| Integer::from(v);
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-1), i(0), i(1)]);
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![i(1), i(2), i(1)]);
let g = gcd_modular_z(&a, &b);
assert_eq!(g.coeffs(), &[i(1), i(1)]); // x + 1
// Output: x + 1
}

See also: Modular GCD algorithm (Brown 1971)


DenseUnivariatePolynomial::content

Signature: pub fn content(&self) -> D::Element

Description: Computes the content of the polynomial (the GCD of all coefficients). The content of the zero polynomial is zero.


DenseUnivariatePolynomial::primitive_part

Signature: pub fn primitive_part(&self) -> Self

Description: Returns the primitive part (the polynomial divided by its content).


7. Resultants

DenseUnivariatePolynomial::resultant

Signature: pub fn resultant(&self, other: &Self) -> D::Element

Description: Computes the resultant $\operatorname{Res}(a, b)$ of two univariate polynomials using the Brown PRS (polynomial remainder sequence) algorithm.

Parameters:

ParameterTypeDescription
other&SelfThe other polynomial

Returns: D::Element — the resultant scalar. It is zero if and only if $\gcd(a, b)$ is non-constant.

Algorithm: subresultant PRS (Brown), with an exact division by $\beta$ at each step (the subresultant theorem guarantees exactness in a UFD).

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// Res(x - 1, x - 2) = 1 - 2 = -1
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(1),
]);
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-2), Integer::from(1),
]);
assert_eq!(a.resultant(&b), Integer::from(-1));
// Output: -1
}

Example (with a common root):

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// x - 1 and x^2 - 1 share the root x = 1
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(1),
]);
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(-1), Integer::from(0), Integer::from(1),
]);
assert_eq!(a.resultant(&b), Integer::from(0));
// Output: 0 (they share the factor x - 1)
}

See also: Resultants and subresultant PRS


8. Rational Function Fraction Field

RationalPolynomial

Signature:

#![allow(unused)]
fn main() {
pub struct RationalPolynomial<D: Domain, O: MonomialOrder = Grevlex> {
    pub numerator: SparseMultivariatePolynomial<D, O>,
    pub denominator: SparseMultivariatePolynomial<D, O>,
}
}

Description: An element of the polynomial fraction field $\frac{n}{d}$, always kept in canonical form: coprime numerator and denominator, with the denominator's leading coefficient positive (ordered domains) or 1 (finite fields).

Construction

RationalPolynomial::new
#![allow(unused)]
fn main() {
pub fn new(
    numerator: SparseMultivariatePolynomial<D, O>,
    denominator: SparseMultivariatePolynomial<D, O>,
) -> Self
}

Creates a rational polynomial. Does not reduce automatically — use from_num_den when a canonical form is needed.

RationalPolynomial::from_num_den
#![allow(unused)]
fn main() {
pub fn from_num_den(
    numerator: SparseMultivariatePolynomial<D, O>,
    denominator: SparseMultivariatePolynomial<D, O>,
) -> Self
}

Creates from numerator and denominator and reduces automatically (GCD simplification + denominator leading-coefficient normalization).

RationalPolynomial::from_polynomial
#![allow(unused)]
fn main() {
pub fn from_polynomial(poly: SparseMultivariatePolynomial<D, O>) -> Self
}

Creates from a polynomial (denominator = 1).

Query Methods

MethodSignatureDescription
is_zero&self -> boolWhether the numerator is zero
is_one&self -> boolWhether it is 1/1
n_vars&self -> usizeNumber of variables
domain&self -> &DCoefficient domain reference

Arithmetic Operations

MethodSignatureDescription
add(&self, &Self) -> SelfAddition (denominator-GCD strategy to reduce intermediate swell)
sub(&self, &Self) -> SelfSubtraction
mul(&self, &Self) -> SelfMultiplication (cross-cancellation)
div(&self, &Self) -> Option<Self>Division (returns None when the divisor's numerator is zero)
neg&self -> SelfNegation $-\frac{n}{d}$
inv&self -> Option<Self>Inverse (returns None when the numerator is zero)
pow(&self, k: u32) -> SelfPower $\left(\frac{n}{d}\right)^k$

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::rational::RationalPolynomial;
use ocas_poly::Grevlex;

let d = IntegerDomain;
let n_vars = 2;

// x / y
let num: SparseMultivariatePolynomial<_, Grevlex> = SparseMultivariatePolynomial::from_terms(d, n_vars,
    vec![(vec![1, 0], Integer::from(1))]);
let den: SparseMultivariatePolynomial<_, Grevlex> = SparseMultivariatePolynomial::from_terms(d, n_vars,
    vec![(vec![0, 1], Integer::from(1))]);
let f = RationalPolynomial::from_num_den(num, den);

// y / x
let num2: SparseMultivariatePolynomial<_, Grevlex> = SparseMultivariatePolynomial::from_terms(d, n_vars,
    vec![(vec![0, 1], Integer::from(1))]);
let den2: SparseMultivariatePolynomial<_, Grevlex> = SparseMultivariatePolynomial::from_terms(d, n_vars,
    vec![(vec![1, 0], Integer::from(1))]);
let g = RationalPolynomial::from_num_den(num2, den2);

// (x/y) * (y/x) = 1
let h = f.mul(&g);
assert!(h.is_one());
// Output: 1
}

See also: RationalPolynomial definition


9. Partial Fraction Decomposition

apart

Signature:

#![allow(unused)]
fn main() {
pub fn apart<D: EuclideanDomain>(
    num: &DenseUnivariatePolynomial<D>,
    den: &DenseUnivariatePolynomial<D>,
) -> (
    Option<DenseUnivariatePolynomial<D>>,
    Vec<PartialFractionTerm<D>>,
)
}

Description: Computes the partial fraction decomposition of $\frac{\text{num}}{\text{den}}$.

Parameters:

ParameterTypeDescription
num&DenseUnivariatePolynomial<D>The numerator polynomial
den&DenseUnivariatePolynomial<D>The denominator polynomial (nonzero)

Returns: (Option<poly_part>, Vec<PartialFractionTerm<D>>) — an optional polynomial part and a list of partial fraction terms, satisfying:

$$\frac{\text{num}}{\text{den}} = \text{poly_part} + \sum_i \frac{\text{numer}_i}{\text{denom}_i^{\text{exp}_i}}$$

Example:

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_calc::partial_fraction::apart;

let d = RationalDomain;
// 1 / (x^2 - 1)
let num = DenseUnivariatePolynomial::from_coeffs(d, vec![Rational::new(1, 1)]);
let den = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Rational::new(-1, 1), Rational::new(0, 1), Rational::new(1, 1),
]);
let (poly_part, terms) = apart(&num, &den);
assert!(poly_part.is_none()); // proper fraction, no polynomial part
// x^2 - 1 = (x-1)(x+1) is square-free → the output groups by factor
// Output: poly_part = None, terms = [...]
}

PartialFractionTerm

Signature:

#![allow(unused)]
fn main() {
pub struct PartialFractionTerm<D: EuclideanDomain> {
    pub numer: DenseUnivariatePolynomial<D>,  // numerator polynomial
    pub denom: DenseUnivariatePolynomial<D>,  // irreducible (square-free) denominator factor
    pub exp: usize,                            // multiplicity of this factor in the original denominator
}
}

Description: A single term of a partial fraction decomposition, representing $\frac{\text{numer}}{\text{denom}^{\text{exp}}}$.


together

Signature:

#![allow(unused)]
fn main() {
pub fn together<D: EuclideanDomain>(
    poly_part: Option<&DenseUnivariatePolynomial<D>>,
    terms: &[PartialFractionTerm<D>],
) -> (DenseUnivariatePolynomial<D>, DenseUnivariatePolynomial<D>)
}

Description: Combines the polynomial part and partial fraction terms into a single rational function $\frac{n}{d}$. The inverse of apart.

Parameters:

ParameterTypeDescription
poly_partOption<&DenseUnivariatePolynomial<D>>The polynomial part (optional)
terms&[PartialFractionTerm<D>]The list of partial fraction terms

Returns: (numerator, denominator) — the combined numerator and denominator.


10. Univariate Polynomial Helper Methods

DenseUnivariatePolynomial::extended_gcd_poly

Signature: pub fn extended_gcd_poly(&self, other: &Self) -> (Self, Self, Self)

Description: Extended Euclidean algorithm. Returns $(g, s, t)$ such that $s \cdot a + t \cdot b = g = \gcd(a, b)$.

Parameters:

ParameterTypeDescription
other&SelfThe other polynomial

Returns: (gcd, s, t).


DenseUnivariatePolynomial::div_rem

Signature: pub fn div_rem(&self, divisor: &Self) -> Option<(Self, Self)>

Description: Division with remainder. Returns (quotient, remainder) such that self = quotient * divisor + remainder. Returns None if the divisor is zero.


DenseUnivariatePolynomial::pow

Signature: pub fn pow(&self, n: u32) -> Self

Description: Computes $\text{self}^n$ by repeated squaring.


DenseUnivariatePolynomial::p_adic_expansion

Signature: pub fn p_adic_expansion(&self, p: &Self) -> Vec<Self>

Description: Computes the $p$-adic expansion of the polynomial with respect to the base polynomial $p$. Returns the coefficient list $[c_0, c_1, \dots]$ such that $\text{self} = \sum_i c_i \cdot p^i$, where $\deg(c_i) < \deg(p)$.


11. Monomial Utility Functions

monomial_divides

Signature: pub fn monomial_divides(a: &[usize], b: &[usize]) -> bool

Description: Checks whether the monomial $b$ divides $a$ (i.e., $a$ is a multiple of $b$: $a_i \geq b_i$ for all $i$).


monomial_lcm

Signature: pub fn monomial_lcm(a: &[usize], b: &[usize]) -> SmallVec<[usize; 4]>

Description: Computes the least common multiple of two monomials (component-wise maximum).


monomial_are_coprime

Signature: pub fn monomial_are_coprime(a: &[usize], b: &[usize]) -> bool

Description: Checks whether two monomials are coprime (no variable appears in both).


12. Internal Algorithm Overview

Hensel Lifting and Zassenhaus Recombination

File: ocas-poly/src/factor/hensel.rs

mignotte_bound

#![allow(unused)]
fn main() {
pub(crate) fn mignotte_bound(f: &ZPoly) -> Integer
}

Landau–Mignotte bound: for a degree-$n$ polynomial $f$, any factor $g$ satisfies $|g|_\infty \leq 2^n |f|_2$.

factor_square_free_monic

#![allow(unused)]
fn main() {
pub fn factor_square_free_monic(f: &ZPoly) -> Vec<ZPoly>
}

Factors a monic square-free $\mathbb{Z}[x]$ polynomial into monic irreducible factors.

factor_square_free

#![allow(unused)]
fn main() {
pub fn factor_square_free(f: &ZPoly) -> Vec<ZPoly>
}

Factors a square-free primitive $\mathbb{Z}[x]$ polynomial into irreducible factors. Non-monic input is handled via the leading-coefficient transformation $a^{d-1} f(x/a)$.

factor_primitive

#![allow(unused)]
fn main() {
pub fn factor_primitive(f: &ZPoly) -> Vec<(ZPoly, usize)>
}

Factors a primitive $\mathbb{Z}[x]$ polynomial into irreducible factors with multiplicities (Yun square-free factorization + factor_square_free_monic). This is the internal implementation of DenseUnivariatePolynomial::factor().


EEZ Hensel Lifting

File: ocas-poly/src/factor/eez.rs

Key internal functions:

FunctionDescription
eez_liftGeneric EEZ lifting (over a field, monic)
eez_lift_imposedNon-monic EEZ lifting (Wang leading-coefficient imposition)
eez_lift_zInteger EEZ lifting (solve over $\mathbb{Q}$ + integrality check)
coefficient_hensel_lift_zp-adic coefficient Hensel lifting
diophantineRecursive multivariate Diophantine equation solver
sparse_diophantine_fpSparse Diophantine solver with skeleton interpolation
wang_reconstruct_lcoeffsWang leading-coefficient reconstruction
zassenhaus_multivariateMultivariate Zassenhaus subset recombination

Finite Field Factorization

File: ocas-poly/src/factor/finite_field.rs

FunctionDescription
factor_over_finite_fieldTop-level entry: square-free + DDF + EDF
distinct_degree_factorizationDDF: group by the degree of the irreducible factors
equal_degree_factorizationEDF: randomly split the product of equal-degree factors
berlekampBerlekamp's algorithm (small primes)
cantor_zassenhausDDF + EDF combination
square_free_factorization_ffCharacteristic-$p$ square-free factorization (Musser/Bernardin)
pth_root_prime$p$-th roots over $\mathbb{F}_p$

Algebraic Number Field Factorization

File: ocas-poly/src/factor/algebraic.rs

FunctionDescription
factor_anfTop-level entry: square-free + Trager
factor_square_free_anfTrager: norm → $\mathbb{Q}$ factorization → GCD recovery
norm_with_shiftTrager shift to find a square-free norm
norm_eval_interpNorm computation by evaluation–interpolation
gcd_anfModular GCD over $\mathbb{Q}(\alpha)$
square_free_anfYun square-free factorization (modular GCD)
factor_square_free_rationalsSquare-free factorization over $\mathbb{Q}$ (clear denominators → $\mathbb{Z}$ Hensel)

See also

Rust API Reference: Numerical Integration

This chapter covers the numerical integration functionality of the ocas-eval crate, centered on the Vegas adaptive Monte Carlo integrator (Lepage 1978). All interfaces are exported through the ocas_eval::numeric module.

Module overview

#![allow(unused)]
fn main() {
use ocas_eval::numeric::{
    Vegas,                // adaptive Monte Carlo integrator
    VegasOptions,         // configuration parameters
    IntegrateResult,      // integration result
    Integrator,           // integrator trait
    integrate_1d,         // 1-D convenience function
    StatisticsAccumulator, // inverse-variance weighted statistics accumulator
};
}

IntegrateResult

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy)]
pub struct IntegrateResult {
    pub integral: f64,
    pub error: f64,
}
}

Description: The return result of numerical integration, containing the integral estimate and its standard error.

Fields:

FieldTypeDescription
integralf64The best estimate of the integral
errorf64The standard error estimate of integral

Example:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::integrate_1d;

let r = integrate_1d(|x| x * x, 0.0, 1.0, Default::default());
println!("∫₀¹ x² dx ≈ {:.6} ± {:.6}", r.integral, r.error);
// Output: ∫₀¹ x² dx ≈ 0.3334 ± 0.0003 (exact value 1/3)
}

Integrator

Signature:

#![allow(unused)]
fn main() {
pub trait Integrator {
    fn integrate<F: Fn(&[f64]) -> f64>(&mut self, f: &F) -> IntegrateResult;
}
}

Description: The unified trait for numerical integrators. The integrand f receives a point in the unit hypercube $[0,1]^d$ and returns a scalar value. Integrating over a physical domain requires performing the linear change of variables manually inside the closure.

Parameters:

ParameterTypeDescription
f&F where F: Fn(&[f64]) -> f64The integrand; the input is a coordinate point in $[0,1]^d$

Returns: IntegrateResult — the integral estimate and its standard error.

Note: integrate takes &F rather than F, so the same closure can be called multiple times without reallocation.


VegasOptions

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy)]
pub struct VegasOptions {
    pub n_bins: usize,
    pub n_samples: usize,
    pub iterations: usize,
    pub learning_rate: f64,
    pub seed: u64,
}
}

Description: Tuning parameters for the Vegas integrator.

Fields:

FieldTypeDefaultDescription
n_binsusize64The number of bins per dimension (the overall grid is the direct product of per-dimension 1-D grids)
n_samplesusize10_000The number of samples per iteration
iterationsusize10The number of adaptive iterations
learning_ratef641.5Grid smoothing/learning rate (typical range 1.0–2.0; larger values make grid updates gentler)
seedu640x0C45Random seed (deterministic across runs)

Implementation details:

  • The Default implementation provides the defaults above.
  • Role of the learning rate: during grid updates, each bin's importance value $d_i$ is transformed into $d_i^{1/\text{learning_rate}}$. learning_rate = 1.0 is the full step; larger values make grid changes smoother and more stable.
  • Uses the Xoshiro256PlusPlus pseudo-random number generator (initialized via seed) for reproducible results.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::VegasOptions;

// High-precision configuration
let opts = VegasOptions {
    n_bins: 100,
    n_samples: 100_000,
    iterations: 20,
    learning_rate: 1.5,
    seed: 42,
};

// Use the defaults
let default_opts = VegasOptions::default();
}

See also: Vegas::new


Vegas

Signature:

#![allow(unused)]
fn main() {
pub struct Vegas { /* private fields */ }
}

Description: An adaptive Monte Carlo integrator based on the Lepage Vegas algorithm, operating on the unit hypercube $[0,1]^d$.

Design rationale: Vegas maintains a product grid (independent 1-D bins per dimension) and refines it iteratively so that each bin captures an approximately equal share of the integrand's variance. Estimates from multiple iterations are combined with inverse-variance weighting (see StatisticsAccumulator).

Vegas::new

Signature:

#![allow(unused)]
fn main() {
pub fn new(n_dims: usize, opts: VegasOptions) -> Self
}

Description: Creates a Vegas integrator in n_dims dimensions.

Parameters:

ParameterTypeDescription
n_dimsusizeThe number of integration dimensions (≥ 1)
optsVegasOptionsTuning parameters

Returns: An initialized Vegas instance with uniform bins in every dimension.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::{Vegas, VegasOptions};

// Two-dimensional integrator with default options
let mut vegas = Vegas::new(2, VegasOptions::default());
}

Vegas::integrate

Signature:

#![allow(unused)]
fn main() {
pub fn integrate<F: Fn(&[f64]) -> f64>(&mut self, f: &F) -> IntegrateResult
}

Description: Runs the integration. For each iteration: sample n_samples points on the unit hypercube, evaluate the integrand, accumulate statistics, then update the grid boundaries to improve sampling efficiency.

Parameters:

ParameterTypeDescription
f&F where F: Fn(&[f64]) -> f64The integrand; the input is a coordinate in $[0,1]^d$

Returns: IntegrateResult — the inverse-variance weighted average estimate over all iterations and its standard error.

Algorithm steps (per iteration):

  1. Sampling: for each dimension, pick a bin uniformly and sample uniformly within the bin; the Jacobian is n_bins × bin_width (i.e., the inverse-pdf contribution).
  2. Evaluation: compute $f(\mathbf{x})$ and accumulate it into the StatisticsAccumulator weighted by the total Jacobian.
  3. Training signal: accumulate $f^2 \cdot w$ into the training slot of the corresponding bin.
  4. Statistics finalization: call finalize_iteration() to fold this iteration's estimate into the cross-iteration inverse-variance weighted average.
  5. Grid update: for each dimension, perform a smooth cumulative-arc-length redistribution — 3-bin smoothing, mean normalization, damping $d^{1/\text{lr}}$, redistributing boundaries at equal-arc-length quantiles, and monotonicity clamping.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::{Vegas, VegasOptions};

// Compute ∫₀¹∫₀¹ sin(x·y) dx dy
let opts = VegasOptions {
    n_samples: 50_000,
    iterations: 15,
    ..Default::default()
};
let mut vegas = Vegas::new(2, opts);
let r = vegas.integrate(&|x: &[f64]| (x[0] * x[1]).sin());
println!("Result: {:.6} ± {:.6}", r.integral, r.error);
// Result: ≈ 0.2398 ± 0.0002
}

Note: The integrand receives coordinates in $[0,1]^d$. To integrate over a physical domain $[a,b]$, perform the linear change $x_{\text{phys}} = a + u \cdot (b-a)$ inside the closure and multiply by the Jacobian $(b-a)$. In one dimension, integrate_1d can be used directly.


Vegas::result

Signature:

#![allow(unused)]
fn main() {
pub fn result(&self) -> IntegrateResult
}

Description: Returns the accumulated estimate after the most recent integrate call.

Returns: IntegrateResult — the current best integral estimate and its standard error.


Vegas::iterations

Signature:

#![allow(unused)]
fn main() {
pub fn iterations(&self) -> usize
}

Description: Returns the number of completed iterations.


integrate_1d

Signature:

#![allow(unused)]
fn main() {
pub fn integrate_1d<F: Fn(f64) -> f64>(
    f: F,
    a: f64,
    b: f64,
    opts: VegasOptions,
) -> IntegrateResult
}

Description: A convenience function for one-dimensional numerical integration. Integrates $f(x)$ over the physical interval $[a, b]$, automatically handling the change of variables $u \mapsto a + u(b-a)$ and folding the Jacobian $(b-a)$ into the result.

Parameters:

ParameterTypeDescription
fF where F: Fn(f64) -> f64The integrand; receives the physical coordinate $x \in [a, b]$ directly
af64The lower limit of integration
bf64The upper limit of integration (may be less than a, in which case the result is negative)
optsVegasOptionsVegas parameters (use Default::default() for the defaults)

Returns: IntegrateResult — the integral estimate and its standard error.

Implementation details: creates a one-dimensional Vegas instance, wraps f as |u| f(a + u[0] * width) * width, and calls integrate.

Example:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::{integrate_1d, VegasOptions};

// ∫₀¹ x dx = 1/2
let r = integrate_1d(|x| x, 0.0, 1.0, Default::default());
assert!((r.integral - 0.5).abs() < 0.01);

// ∫₀² x² dx = 8/3 ≈ 2.667
let r = integrate_1d(|x| x * x, 0.0, 2.0, Default::default());
assert!((r.integral - 8.0 / 3.0).abs() < 0.05);

// High-precision configuration
let opts = VegasOptions {
    n_samples: 100_000,
    iterations: 20,
    ..Default::default()
};
let r = integrate_1d(|x| (-x * x).exp(), -5.0, 5.0, opts);
// ∫₋₅⁵ e^{-x²} dx ≈ √π ≈ 1.7725
assert!((r.integral - std::f64::consts::PI.sqrt()).abs() < 0.01);
}

See also: Vegas::new, Vegas::integrate


StatisticsAccumulator

Signature:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct StatisticsAccumulator { /* private fields */ }
}

Description: An inverse-variance weighted statistics accumulator for combining Monte Carlo integration estimates across iterations. Implements the weighting scheme from Lepage's original Vegas paper.

Design rationale: Each iteration produces an integral estimate $I_i$ and a standard error $\sigma_i$. Iterations are combined with inverse-variance weights $w_i = 1/\sigma_i^2$:

$$ I_{\text{combined}} = \frac{\sum_i w_i I_i}{\sum_i w_i}, \quad \sigma_{\text{combined}} = \frac{1}{\sqrt{\sum_i w_i}} $$

A $\chi^2$ statistic $\sum_i w_i (I_i - I_{\text{combined}})^2$ is also maintained to diagnose the quality of the grid stratification — $\chi^2 \approx \text{iterations} - 1$ indicates that the per-iteration estimates are consistent.

Internal fields (private):

FieldTypeDescription
sum_wf64Current iteration: $\sum \text{weight}$
sum_wff64Current iteration: $\sum \text{weight} \cdot f$
sum_wf2f64Current iteration: $\sum \text{weight} \cdot f^2$
integralf64The cross-iteration weighted average integral estimate
errorf64The standard error of the integral estimate
chi_squaref64The cross-iteration $\chi^2$ statistic
iterationsusizeThe number of completed iterations

StatisticsAccumulator::new

Signature:

#![allow(unused)]
fn main() {
pub fn new() -> Self
}

Description: Creates an empty accumulator. Initial state: integral = 0.0, error = INFINITY, iterations = 0.


StatisticsAccumulator::add_sample

Signature:

#![allow(unused)]
fn main() {
pub fn add_sample(&mut self, weight: f64, f: f64)
}

Description: Adds a sample point. In Vegas, weight is the Jacobian (inverse pdf) at the point and f is the integrand value. The sample's contribution to this iteration's integral estimate is weight * f.

Parameters:

ParameterTypeDescription
weightf64The sample weight (in Vegas, the Jacobian = the product of n_bins × bin_width over all dimensions)
ff64The value of the integrand at the point

Internal accumulation: sum_w += weight, sum_wf += weight * f, sum_wf2 += weight * f².


StatisticsAccumulator::finalize_iteration

Signature:

#![allow(unused)]
fn main() {
pub fn finalize_iteration(&mut self)
}

Description: Finalizes the current iteration — computes this iteration's mean and variance, folds them into the cross-iteration average with inverse-variance weighting, then resets this iteration's accumulators.

This iteration's mean: $\bar{f} = \sum(w \cdot f) / \sum w$

This iteration's variance: $\text{Var} = \sum(w \cdot f^2) / \sum w - \bar{f}^2$

Degenerate handling: if sum_w ≤ 0 or the variance is negative (numerical issues), the iteration is skipped but still reset.

Error clamping: the standard error is clamped to be no smaller than $10^{-150}$ (to avoid overflow in the inverse-variance weights).


StatisticsAccumulator::samples

Signature:

#![allow(unused)]
fn main() {
pub fn samples(&self) -> usize
}

Description: Returns the approximate number of samples in the current (unfinalized) iteration.

Note: derived internally from sum_w (sum_w as usize); exact when the weights are 1. Vegas's Jacobian weights make this value approximate.


StatisticsAccumulator::integral

Signature:

#![allow(unused)]
fn main() {
pub fn integral(&self) -> f64
}

Description: The current best integral estimate (the cross-iteration inverse-variance weighted average).


StatisticsAccumulator::error

Signature:

#![allow(unused)]
fn main() {
pub fn error(&self) -> f64
}

Description: The standard error of the integral estimate, i.e., $\sigma_{\text{combined}} = 1/\sqrt{\sum w_i}$.


StatisticsAccumulator::chi_square

Signature:

#![allow(unused)]
fn main() {
pub fn chi_square(&self) -> f64
}

Description: The cross-iteration $\chi^2$ statistic. Values close to iterations - 1 indicate that the per-iteration estimates are consistent (good grid stratification); values significantly larger indicate structure in the integrand not captured by the grid.


StatisticsAccumulator::iterations

Signature:

#![allow(unused)]
fn main() {
pub fn iterations(&self) -> usize
}

Description: The number of completed iterations.


Full example:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::StatisticsAccumulator;

let mut acc = StatisticsAccumulator::new();

// Simulate two iterations with three samples each
// First iteration
acc.add_sample(1.0, 1.5);
acc.add_sample(1.0, 1.8);
acc.add_sample(1.0, 2.1);
acc.finalize_iteration();
assert_eq!(acc.iterations(), 1);

// Second iteration
acc.add_sample(1.0, 1.6);
acc.add_sample(1.0, 1.9);
acc.add_sample(1.0, 2.0);
acc.finalize_iteration();
assert_eq!(acc.iterations(), 2);

// Cross-iteration weighted average
println!("Integral: {:.4} ± {:.4}", acc.integral(), acc.error());
println!("χ² = {:.2}", acc.chi_square());
}

See also: Vegas::integrate (uses this accumulator internally)


Working with ExpressionEvaluator

The Vegas integrator can be combined with ExpressionEvaluator to numerically integrate symbolic expressions:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::{Vegas, VegasOptions};
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.fun("sin", &[ctx.var("x")]);

let evaluator: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();
let mut vegas = Vegas::new(1, VegasOptions::default());
let r = vegas.integrate(&|x: &[f64]| {
    evaluator.evaluate(&[x[0]]).unwrap()[0]
});
}

For one-dimensional cases, the integrate_1d wrapper can be used directly:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::integrate_1d;
use ocas_eval::ExpressionEvaluator;
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.fun("sin", &[ctx.var("x")]);
let evaluator: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(expr).unwrap();

let r = integrate_1d(
    |x| evaluator.evaluate(&[x]).unwrap()[0],
    0.0,
    std::f64::consts::PI,
    Default::default(),
);
// ∫₀^π sin(x) dx ≈ 2.0
}

See also: Evaluation and JIT, Monte Carlo integration foundations

Python API Reference

This chapter documents the complete API of the oCAS Python bindings (the ocas module). Every class and function includes its signature, parameter descriptions, return value, exceptions, and complete examples.

Version: This document corresponds to oCAS 0.24.x.

Import

import ocas

The module exports all classes and functions at the top level; no submodule imports are required.


Expressions

Expression

Symbolic expression class. Constructed by parsing a string; supports arithmetic operations, calculus, simplification, and more.

Signature:

ocas.Expression(input: str)

Parameters:

ParameterTypeDescription
inputstrSymbolic expression string; ^ denotes exponentiation and * denotes multiplication

Returns: an Expression instance.

Exceptions: ValueError — the expression string failed to parse.

Supported operations:

OperationMethodDescription
Addition__add__e1 + e2
Subtraction__sub__e1 - e2
Multiplication__mul__e1 * e2
Power__pow__e1 ** e2
Negation__neg__-e
Equality__eq__e1 == e2 (comparison based on the normalized form)
Hash__hash__Usable as a dictionary key or set element
String__str__Outputs the internal representation
Representation__repr__Expression("...") format

Methods:


Expression.clone

Expression.clone() -> Expression

Returns a deep copy of the expression.

Example:

>>> e = ocas.Expression("x + 1")
>>> f = e.clone()
>>> e == f
True

Expression.simplify

Expression.simplify() -> Expression

Simplifies the expression using the built-in rule set (fixed-point iteration).

Returns: the simplified Expression.

Exceptions: ValueError — an internal error occurred during simplification.

Example:

>>> e = ocas.Expression("x + x")
>>> print(e.simplify())
2*x

Expression.diff

Expression.diff(var: str) -> Expression

Differentiates the expression with respect to the variable var.

Parameters:

ParameterTypeDescription
varstrThe differentiation variable

Returns: the derivative expression.

Example:

>>> e = ocas.Expression("x^3 + 2*x")
>>> print(e.diff("x"))
2 + (3*(x^2))

Expression.integrate

Expression.integrate(var: str) -> Expression

Symbolically integrates the expression with respect to the variable var. Uses a layered integration pipeline: fast lookup table → rational functions → Risch → trigonometric rewrite + realify → special functions → heuristic → unevaluated form.

Parameters:

ParameterTypeDescription
varstrThe integration variable

Returns: the resulting expression. If no closed form can be found, returns the unevaluated Integral(expr, var) form. The result is not certified; use integrate_outcome when a machine-checkable certificate is required.

Example:

>>> e = ocas.Expression("x^2")
>>> print(e.integrate("x"))
(3^-1)*(x^3)

Expression.integrate_outcome

Expression.integrate_outcome(var: str, rules: bool = True) -> dict

Certified integration (new in 0.28.0): the result is three-valued. found always carries a machine-checkable symbolic certificate (D(F) − f ≡ 0), while unknown honestly returns the unevaluated form. A result the exact checker falsifies is never returned as found.

Parameters:

ParameterTypeDescription
varstrThe integration variable
rulesboolWhether the rule-table engine is enabled (default True)

Returns: a dict with the keys:

KeyDescription
kind"found" / "proved_nonelementary" / "unknown"
valueThe antiderivative, or the unevaluated Integral(expr, var) form when unknown
certificateThe certificate layer ("structural" / "field" / "radical"), None when uncertified
uncertifiedThe pipeline's uncertified candidate (diagnostics only — explicitly not an answer), None when absent

Example:

>>> e = ocas.Expression("2*x")
>>> e.integrate_outcome("x")["kind"]
'found'
>>> ocas.Expression("exp(x^3)").integrate_outcome("x")["kind"]
'unknown'

Expression.integrate_heuristic

Expression.integrate_heuristic(var: str) -> Expression

Integrates using heuristic methods only (integration by parts, trigonometric substitutions, etc.), without invoking the full Risch algorithm. Returns a closed form on success and an unevaluated form on failure.

Parameters:

ParameterTypeDescription
varstrThe integration variable

Returns: the integral result or the unevaluated Integral(expr, var).

Example:

>>> e = ocas.Expression("sin(x)")
>>> print(e.integrate_heuristic("x"))
-1*(cos(x))

Expression.taylor

Expression.taylor(var: str, point: Expression, order: int) -> Expression

Computes the Taylor expansion of the expression in var about point up to order order.

Parameters:

ParameterTypeDescription
varstrThe expansion variable
pointExpressionThe expansion center
orderintExpansion order (a non-negative integer)

Returns: the Taylor polynomial.

Example:

>>> e = ocas.Expression("exp(x)")
>>> p = ocas.Expression("0")
>>> print(e.taylor("x", p, 4))
1 + x + ((2^-1)*(x^2)) + ((6^-1)*(x^3)) + ((24^-1)*(x^4))

Expression.substitute

Expression.substitute(var: str, replacement: Expression) -> Expression

Replaces every occurrence of var in the expression with replacement.

Parameters:

ParameterTypeDescription
varstrThe variable to be replaced
replacementExpressionThe replacement expression

Returns: the substituted expression.

Example:

>>> e = ocas.Expression("x^2 + y")
>>> r = ocas.Expression("3")
>>> print(e.substitute("x", r))
y + (3^2)

Polynomials

Polynomial

Dense univariate polynomial supporting three coefficient domains: integers ℤ, rationals ℚ, and finite fields GF(p).

Signature:

ocas.Polynomial(coeffs, domain=None)

Parameters:

ParameterTypeDescription
coeffslistCoefficient list, ordered from lowest to highest degree
domainstr, FiniteField, or NoneCoefficient domain: "integer" (default), "rational", or FiniteField(p)

Coefficient formats:

  • Integer domain: a list of integers, e.g. [1, 2, 1] represents $1 + 2x + x^2$
  • Rational domain: a list of integers or a list of (numerator, denominator) tuples
  • Finite field: a list of integers; arithmetic is automatically reduced modulo p

Exceptions: TypeError — coefficient types do not match the selected domain.

Example:

>>> from ocas import Polynomial
>>> p = Polynomial([1, 2, 1])
>>> print(p.degree())
2

Polynomial.coeffs

Polynomial.coeffs() -> list[str]

Returns the coefficient list (as strings), ordered from lowest to highest degree.

Example:

>>> p = Polynomial([1, 2, 1])
>>> p.coeffs()
['1', '2', '1']

Polynomial.degree

Polynomial.degree() -> int | None

Returns the degree of the polynomial. The zero polynomial returns None.

Example:

>>> Polynomial([1, 2, 1]).degree()
2
>>> Polynomial([0]).degree() is None
True

Polynomial.len

Polynomial.len() -> int

Returns the number of stored coefficients.


Polynomial.is_zero

Polynomial.is_zero() -> bool

Whether the polynomial is the zero polynomial.


Polynomial.eval

Polynomial.eval(x) -> str

Evaluates the polynomial at the point x. Over the integer domain x is an integer; over the rational domain x is an integer or a (numerator, denominator) tuple.

Returns: the string representation of the evaluation result.

Example:

>>> p = Polynomial([1, 2, 1])
>>> p.eval(2)
'9'

Polynomial.derivative

Polynomial.derivative() -> Polynomial

Returns the formal derivative.

Example:

>>> p = Polynomial([1, 2, 1])
>>> q = p.derivative()
>>> q.coeffs()
['2', '2']

Polynomial.integral

Polynomial.integral() -> Polynomial

Returns the formal integral (with zero constant term). Available on all three coefficient domains: exact over the rationals; over the integers and finite fields, coefficients whose division by $i+1$ does not divide exactly are set to zero (no exception is raised).


Polynomial.primitive_part

Polynomial.primitive_part() -> Polynomial

Returns the primitive part (content removed). Supported on the integer domain only.

Exceptions: ValueError — the polynomial is not over the integer domain.


Polynomial.factor

Polynomial.factor() -> list[PolynomialFactor]

Complete factorization. Returns a list of (factor, multiplicity) pairs: over the integers each factor is primitive; over a finite field each factor is monic.

Factorization strategy:

  • ℤ[x]: square-free factorization + Berlekamp–Zassenhaus + Hensel lifting
  • GF(p)[x]: Berlekamp's algorithm

Example:

>>> p = Polynomial([1, 2, 1])
>>> for f in p.factor():
...     print(f.factor.coeffs(), f.multiplicity)
['1', '1'] 2

Polynomial.square_free_factorization

Polynomial.square_free_factorization() -> list[PolynomialFactor]

Square-free factorization. Returns the pairwise-distinct square-free factors together with their multiplicities.

Example:

>>> p = Polynomial([1, 2, 1])
>>> for f in p.square_free_factorization():
...     print(f.factor.coeffs(), f.multiplicity)
['1', '1'] 2

Polynomial.is_square_free

Polynomial.is_square_free() -> bool

Whether the polynomial is square-free (has no repeated irreducible factors).


Polynomial.gcd

Polynomial.gcd(other: Polynomial) -> Polynomial

Computes the greatest common divisor with other. Both polynomials must be over the same coefficient domain.

Exceptions: TypeError — the coefficient domains do not match.

Example:

>>> a = Polynomial([0, 1])         # x
>>> b = Polynomial([0, 0, 1])     # x^2
>>> a.gcd(b).coeffs()
['0', '1']

Polynomial.div_rem

Polynomial.div_rem(other: Polynomial) -> tuple[Polynomial, Polynomial] | None

Division with remainder, returning (quotient, remainder). Returns None when other is zero.

Exceptions: TypeError — the coefficient domains do not match.

Example:

>>> a = Polynomial([1, 2, 1])
>>> b = Polynomial([1, 1])
>>> q, r = a.div_rem(b)
>>> q.coeffs()
['1', '1']
>>> r.is_zero()
True

Polynomial arithmetic

OperationSyntaxDescription
Additionp + qPolynomial addition
Subtractionp - qPolynomial subtraction
Multiplicationp * qPolynomial multiplication
Negation-pNegates the coefficients
Equalityp == qComparison of normalized coefficients

PolynomialFactor

A single factor in a factorization result.

Attributes:

AttributeTypeDescription
factorPolynomialThe factor polynomial (primitive over ℤ, monic over GF(p))
multiplicityintThe multiplicity

MultivariatePolynomial

Sparse multivariate polynomial ($\mathbb{Q}[x_1, \dots, x_n]$), using the lexicographic order (Lex).

Signature:

ocas.MultivariatePolynomial(terms: dict, n_vars: int)

Parameters:

ParameterTypeDescription
termsdict[tuple[int,...], int]Keys are exponent tuples (of length n_vars), values are coefficients
n_varsintThe number of variables

Example:

>>> from ocas import MultivariatePolynomial
>>> # x² + y² - 1
>>> p = MultivariatePolynomial({(2, 0): 1, (0, 2): 1, (0, 0): -1}, n_vars=2)
>>> p.n_vars()
2

MultivariatePolynomial.n_vars

MultivariatePolynomial.n_vars() -> int

Returns the number of variables.


Matrices

Matrix

Dense matrix over a field, supporting ℤ, ℚ, and GF(p).

Signature:

ocas.Matrix(rows, domain=None)

Parameters:

ParameterTypeDescription
rowslist[list]Row-major two-dimensional list
domainstr, FiniteField, or NoneCoefficient domain: "integer" (default), "rational", or FiniteField(p)

Exceptions: TypeError — element types do not match the selected domain.

Example:

>>> from ocas import Matrix
>>> a = Matrix([[1, 2], [3, 4]])
>>> a.determinant()
'-2'

Matrix.nrows

Matrix.nrows -> int  # property

The number of rows.


Matrix.ncols

Matrix.ncols -> int  # property

The number of columns.


Matrix.shape

Matrix.shape() -> tuple[int, int]

Returns (nrows, ncols).


Matrix.getitem

Matrix[i, j] -> str

Returns the element at position (i, j) (as a string).

Exceptions: ValueError — index out of bounds.

Example:

>>> a = Matrix([[1, 2], [3, 4]])
>>> a[1, 0]
'3'

Matrix.rows

Matrix.rows() -> list[list[str]]

Returns the two-dimensional list of all rows (as strings).


Matrix.transpose

Matrix.transpose() -> Matrix

Returns the transposed matrix.


Matrix.trace

Matrix.trace() -> str

Returns the trace of the square matrix (sum of diagonal elements).

Exceptions: ValueError — the matrix is not square.


Matrix.rank

Matrix.rank() -> int

Returns the rank of the matrix.


Matrix.determinant

Matrix.determinant() -> str

Returns the determinant of the square matrix (Bareiss fraction-free algorithm).

Exceptions: ValueError — the matrix is not square.

Example:

>>> a = Matrix([[1, 2], [3, 4]])
>>> a.determinant()
'-2'

Matrix.inverse

Matrix.inverse() -> Matrix

Returns the inverse matrix.

Exceptions: ValueError — the matrix is singular or not square.


Matrix.solve

Matrix.solve(rhs) -> list[str]

Solves the linear system self · x = rhs.

Parameters:

ParameterTypeDescription
rhslistThe right-hand side vector

Returns: the solution vector (as a list of strings).

Exceptions: ValueError — no solution or the solution is not unique.

Example:

>>> a = Matrix([[2, 0], [0, 3]])
>>> a.solve([4, 9])
['2', '3']

Matrix arithmetic

OperationSyntaxDescription
Matrix multiplicationa @ b__matmul__
Additiona + bElement-wise addition
Subtractiona - bElement-wise subtraction
Equalitya == bElement-wise comparison

Coefficient Domains

IntegerDomain

Selector class for the integer domain ℤ.

Signature:

ocas.IntegerDomain()

Note: the domain parameter of Polynomial and Matrix currently accepts strings ("integer" / "rational") or a FiniteField instance; IntegerDomain() / RationalDomain() instances cannot yet be passed directly as that parameter.

Example:

>>> from ocas import IntegerDomain, Polynomial
>>> d = IntegerDomain()
>>> repr(d)
'IntegerDomain()'
>>> p = Polynomial([1, 2, 1], domain="integer")
>>> p.coeffs()
['1', '2', '1']

RationalDomain

Selector for the rational number domain ℚ.

Signature:

ocas.RationalDomain()

Example:

>>> from ocas import RationalDomain, Polynomial
>>> d = RationalDomain()
>>> repr(d)
'RationalDomain()'
>>> p = Polynomial([(1, 2), (3, 4)], domain="rational")
>>> p.coeffs()
['1/2', '3/4']

FiniteField

The finite field GF(p), where $p$ is a prime.

Signature:

ocas.FiniteField(modulus: int)

Parameters:

ParameterTypeDescription
modulusintPrime modulus, $p \geq 2$

Returns: a FiniteField instance.

Exceptions: ValueError — modulus < 2.

Attributes:

AttributeTypeDescription
modulusstrDecimal string of the prime modulus

Example:

>>> from ocas import FiniteField, Polynomial
>>> gf5 = FiniteField(5)
>>> p = Polynomial([1, 2, 1], domain=gf5)
>>> print(repr(gf5))
FiniteField(5)

Solvers

solve_linear_rational

ocas.solve_linear_rational(a: list[list[int]], b: list[int]) -> list[tuple[int, int]]

Solves the linear system $A \mathbf{x} = \mathbf{b}$ over ℚ.

Parameters:

ParameterTypeDescription
alist[list[int]]Coefficient matrix (row-major, integers)
blist[int]Right-hand side vector (integers)

Returns: the solution vector, each component being a (numerator, denominator) tuple.

Exceptions: ValueError — the solution is not unique.

Example:

>>> ocas.solve_linear_rational([[2, 0], [0, 3]], [4, 9])
[(2, 1), (3, 1)]

solve_linear_integer

ocas.solve_linear_integer(a: list[list[int]], b: list[int]) -> list[int]

Solves the linear system $A \mathbf{x} = \mathbf{b}$ over ℤ.

Parameters:

ParameterTypeDescription
alist[list[int]]Coefficient matrix (row-major, integers)
blist[int]Right-hand side vector (integers)

Returns: the integer solution vector.

Exceptions: ValueError — no integer solution exists.

Example:

>>> ocas.solve_linear_integer([[2, 0], [0, 3]], [4, 9])
[2, 3]

solve_diophantine

ocas.solve_diophantine(a: int, b: int, c: int) -> DiophantineSolution | None

Solves the linear Diophantine equation $ax + by = c$.

Parameters:

ParameterTypeDescription
aintCoefficient of $x$
bintCoefficient of $y$
cintRight-hand side constant

Returns: a DiophantineSolution (when a solution exists) or None (when no integer solution exists).

Example:

>>> sol = ocas.solve_diophantine(3, 5, 1)
>>> sol.particular
(2, -1)
>>> sol.general
(5, -3)

DiophantineSolution

The solution of a Diophantine equation.

Attributes:

AttributeTypeDescription
particulartuple[int, int]A particular solution $(x_0, y_0)$
generaltuple[int, int]The general-solution direction $(t_x, t_y)$; the general solution is $(x_0 + k \cdot t_x, ; y_0 + k \cdot t_y)$, $k \in \mathbb{Z}$

ODE

classify_ode

ocas.classify_ode(equation: Expression, func: str, var: str) -> list[str]

Classifies an ordinary differential equation and returns the list of available solver method names.

Parameters:

ParameterTypeDescription
equationExpressionThe ODE expression (in the form equal to zero), e.g. Derivative(y(x), x) - y(x)
funcstrName of the unknown function, e.g. "y"
varstrName of the independent variable, e.g. "x"

Returns: a list of method names, e.g. ["LinearFirst", "PowerSeries"].

Supported ODE types: Separable, LinearFirst, Bernoulli, Exact, Homogeneous, LinearConstantCoeff, CauchyEuler, ReductionOfOrder, PowerSeries.

Example:

>>> eq = ocas.Expression("Derivative(y(x), x) - y(x)")
>>> ocas.classify_ode(eq, "y", "x")
['LinearFirst', 'PowerSeries']

dsolve

ocas.dsolve(equation: Expression, func: str, var: str, hint: str | None = None) -> str

Symbolically solves an ODE.

Signature (with defaults): dsolve(equation, func, var, hint=None)

Parameters:

ParameterTypeDescription
equationExpressionThe ODE expression (in the form equal to zero)
funcstrName of the unknown function
varstrName of the independent variable
hintstr or NoneA specific solver method (one of the names returned by classify_ode)

Returns: the string representation of the solution (explicit, implicit, series, or unevaluated form).

Example:

>>> eq = ocas.Expression("Derivative(y(x), x) - y(x)")
>>> ocas.dsolve(eq, "y", "x")
'y = C1*exp(x)'

dsolve_ivp

ocas.dsolve_ivp(equation: Expression, func: str, var: str, y0: str, y1: str | None = None) -> str

Solves an initial value problem for a constant-coefficient linear ODE via the Laplace transform.

Signature (with defaults): dsolve_ivp(equation, func, var, y0, y1=None)

Parameters:

ParameterTypeDescription
equationExpressionThe ODE expression (in the form equal to zero)
funcstrName of the unknown function
varstrName of the independent variable
y0strThe value of $y(0)$ (string expression, e.g. "1")
y1str or NoneThe value of $y'(0)$ (required for second-order problems)

Returns: the explicit solution string without arbitrary constants.

Example:

>>> eq = ocas.Expression("Derivative(y(x), x, 2) + y(x)")
>>> ocas.dsolve_ivp(eq, "y", "x", "0", "1")
'y = sin(x)'

Number Theory

factorint

ocas.factorint(n: int) -> list[tuple[str, int]]

Factors $|n|$ into prime factors. Negative inputs start with ("-1", 1).

Parameters:

ParameterTypeDescription
nintThe integer to be factored (arbitrary precision)

Returns: a list of (prime, exponent) tuples, sorted by prime in ascending order.

Example:

>>> ocas.factorint(60)
[('2', 2), ('3', 1), ('5', 1)]
>>> ocas.factorint(-12)
[('-1', 1), ('2', 2), ('3', 1)]

isprime

ocas.isprime(n: int) -> bool

BPSW probabilistic primality test. Deterministic for $n < 2^{64}$; no composite numbers of any size are currently known to pass.

Parameters:

ParameterTypeDescription
nintThe integer to test (arbitrary precision)

Returns: True if (probably) prime.

Example:

>>> ocas.isprime(97)
True
>>> ocas.isprime(100)
False

isprime_u64

ocas.isprime_u64(n: int) -> bool

Deterministic primality test for $n < 2^{64}$ (u64 fast path).

Parameters:

ParameterTypeDescription
nintThe integer to test (must fit in the u64 range)

Returns: True if prime.

Example:

>>> ocas.isprime_u64(2**61 - 1)
True

nextprime

ocas.nextprime(n: int) -> int

Returns the smallest prime strictly greater than $n$.

Parameters:

ParameterTypeDescription
nintThe starting integer

Returns: the next prime (arbitrary precision).

Example:

>>> ocas.nextprime(10)
11
>>> ocas.nextprime(11)
13

discrete_log

ocas.discrete_log(p: int, base: int, target: int) -> int

Solves the discrete logarithm $\text{base}^x \equiv \text{target} \pmod{p}$. Uses the Pohlig–Hellman algorithm for prime $p$, otherwise BSGS.

Parameters:

ParameterTypeDescription
pintThe modulus
baseintThe base
targetintThe target value

Returns: the discrete logarithm $x$.

Exceptions: ValueError — the logarithm does not exist.

Example:

>>> ocas.discrete_log(13, 2, 9)
8
>>> pow(2, 8, 13)
9

crt

ocas.crt(moduli: list[int], residues: list[int]) -> tuple[int, int]

Chinese remainder theorem: solves the system of congruences $x \equiv r_i \pmod{m_i}$.

Parameters:

ParameterTypeDescription
modulilist[int]List of moduli (need not be pairwise coprime)
residueslist[int]List of residues

Returns: (r, m) such that $x \equiv r \pmod{m}$.

Exceptions: ValueError — the system is inconsistent.

Example:

>>> ocas.crt([3, 5, 7], [2, 3, 2])
(23, 105)

jacobi_symbol

ocas.jacobi_symbol(a: int, n: int) -> int

Computes the Jacobi symbol $\left(\frac{a}{n}\right)$, where $n$ is a positive odd integer.

Parameters:

ParameterTypeDescription
aintNumerator
nintDenominator (positive odd)

Returns: $-1$, $0$, or $1$.

Exceptions: ValueError — n is not a positive odd integer.

Example:

>>> ocas.jacobi_symbol(2, 7)
1
>>> ocas.jacobi_symbol(3, 7)
-1

totient

ocas.totient(n: int) -> int

Euler totient function $\varphi(n)$.

Parameters:

ParameterTypeDescription
nintA positive integer

Returns: $\varphi(n)$ (arbitrary precision).

Example:

>>> ocas.totient(12)
4

mobius

ocas.mobius(n: int) -> int

Möbius function $\mu(n)$.

Parameters:

ParameterTypeDescription
nintA positive integer

Returns: $-1$, $0$, or $1$.

Example:

>>> ocas.mobius(6)
1
>>> ocas.mobius(4)
0

divisor_count

ocas.divisor_count(n: int) -> int

Number of positive divisors $\tau(n)$.

Parameters:

ParameterTypeDescription
nintA positive integer

Returns: the number of positive divisors.

Example:

>>> ocas.divisor_count(12)
6

divisor_sigma

ocas.divisor_sigma(n: int, k: int = 1) -> int

Sum of the $k$-th powers of the divisors $\sigma_k(n) = \sum_{d \mid n} d^k$.

Signature (with defaults): divisor_sigma(n, k=1)

Parameters:

ParameterTypeDescription
nintA positive integer
kintThe exponent (default 1, i.e. the sum of divisors $\sigma_1$)

Returns: $\sigma_k(n)$.

Example:

>>> ocas.divisor_sigma(12)      # 1+2+3+4+6+12
28
>>> ocas.divisor_sigma(12, 2)   # 1+4+9+16+36+144
210

liouville_lambda

ocas.liouville_lambda(n: int) -> int

Liouville function $\lambda(n) = (-1)^{\Omega(n)}$, where $\Omega(n)$ is the number of prime factors of $n$ counted with multiplicity.

Parameters:

ParameterTypeDescription
nintA positive integer

Returns: $-1$ or $1$.

Example:

>>> ocas.liouville_lambda(12)  # Ω(12)=3
-1
>>> ocas.liouville_lambda(9)   # Ω(9)=2
1

Gröbner Bases & Ideals

Note: The following functions keep their py_ prefix (the Rust side does not use #[pyo3(name=...)] renaming). The actual call names in Python are ocas.py_groebner_basis, ocas.py_ideal_contains, etc.

py_groebner_basis

ocas.py_groebner_basis(generators, n_vars: int = 1, algorithm: str = "auto") -> GroebnerBasis

Computes a Gröbner basis of the polynomial ideal.

Parameters:

ParameterTypeDescription
generatorslistList of generators (MultivariatePolynomial or Polynomial objects)
n_varsintNumber of variables (default 1; auto-detected if the generators are MultivariatePolynomial)
algorithmstrAlgorithm selection: "auto" (default), "f4", "f5", "buchberger"

Returns: a GroebnerBasis object.

Exceptions: ValueError — invalid algorithm name.

Example:

>>> from ocas import MultivariatePolynomial, py_groebner_basis
>>> f = MultivariatePolynomial({(2, 0): 1, (0, 0): -1}, n_vars=2)   # x² - 1
>>> g = MultivariatePolynomial({(0, 2): 1, (0, 0): -1}, n_vars=2)   # y² - 1
>>> gb = py_groebner_basis([f, g], n_vars=2)
>>> len(gb)
2

py_ideal_contains

ocas.py_ideal_contains(generators, f, n_vars: int = 1, algorithm: str = "auto") -> bool

Determines whether the polynomial $f$ belongs to the ideal generated by generators.

Parameters:

ParameterTypeDescription
generatorslistList of generators of the ideal
fMultivariatePolynomial or PolynomialThe polynomial to test
n_varsintNumber of variables (default 1)
algorithmstrAlgorithm selection (default "auto")

Returns: True if $f \in I$.

Example:

>>> from ocas import MultivariatePolynomial, py_ideal_contains
>>> f = MultivariatePolynomial({(2, 0): 1, (0, 0): -1}, n_vars=2)
>>> g = MultivariatePolynomial({(0, 2): 1, (0, 0): -1}, n_vars=2)
>>> x = MultivariatePolynomial({(1, 0): 1}, n_vars=2)
>>> py_ideal_contains([f, g], f, n_vars=2)
True
>>> py_ideal_contains([f, g], x, n_vars=2)
False

py_solve_polynomial_system

ocas.py_solve_polynomial_system(equations, n_vars: int = 1, algorithm: str = "auto") -> PolynomialSystemSolution

Solves a system of polynomial equations.

Parameters:

ParameterTypeDescription
equationslistList of equations (each a polynomial in the form equal to zero)
n_varsintNumber of variables (default 1)
algorithmstrAlgorithm selection (default "auto")

Returns: a PolynomialSystemSolution object.

Example:

>>> from ocas import MultivariatePolynomial, py_solve_polynomial_system
>>> f = MultivariatePolynomial({(2, 0): 1, (0, 2): 1, (0, 0): -1}, n_vars=2)
>>> g = MultivariatePolynomial({(1, 0): 1, (0, 0): -1}, n_vars=2)
>>> sol = py_solve_polynomial_system([f, g], n_vars=2)
>>> sol.kind
'zero_dimensional'
>>> for s in sol.solutions():
...     print(s.values, s.multiplicity)
[1.0, 0.0] 1

py_hilbert_series

ocas.py_hilbert_series(gb: GroebnerBasis) -> HilbertSeries

Computes the Hilbert series of the Gröbner basis.

Parameters:

ParameterTypeDescription
gbGroebnerBasisA Gröbner basis

Returns: a HilbertSeries object.

Example:

>>> from ocas import MultivariatePolynomial, py_groebner_basis, py_hilbert_series
>>> f = MultivariatePolynomial({(2, 0): 1, (0, 0): -1}, n_vars=2)
>>> gb = py_groebner_basis([f], n_vars=2)
>>> hs = py_hilbert_series(gb)
>>> hs.dimension
1

py_ideal_radical

ocas.py_ideal_radical(generators, n_vars: int = 1) -> GroebnerBasis

Computes the radical $\sqrt{I}$ of the ideal.

Parameters:

ParameterTypeDescription
generatorslistList of generators of the ideal
n_varsintNumber of variables (default 1)

Returns: a Gröbner basis of $\sqrt{I}$.

Algorithm: square-free factorization for zero-dimensional ideals; Jacobian saturation $\sqrt{I} = I : h^\infty$ for positive-dimensional ideals.


py_primary_decomposition

ocas.py_primary_decomposition(generators, n_vars: int = 1) -> list[PrimaryComponent]

Computes the primary decomposition of the ideal.

Parameters:

ParameterTypeDescription
generatorslistList of generators of the ideal
n_varsintNumber of variables (default 1)

Returns: a list of PrimaryComponent.

Example:

>>> from ocas import MultivariatePolynomial, py_primary_decomposition
>>> f = MultivariatePolynomial({(2, 0): 1, (0, 0): -1}, n_vars=1)
>>> comps = py_primary_decomposition([f], n_vars=1)
>>> len(comps)
2

py_is_zero_dimensional

ocas.py_is_zero_dimensional(gb: GroebnerBasis) -> bool

Determines whether the ideal of the Gröbner basis is zero-dimensional.

Parameters:

ParameterTypeDescription
gbGroebnerBasisA Gröbner basis

Returns: True if zero-dimensional.


py_eliminate

ocas.py_eliminate(generators, elim_vars: int, n_vars: int = 1, algorithm: str = "auto") -> GroebnerBasis

Elimination: computes a Gröbner basis of the elimination ideal $I \cap k[x_{e+1}, \dots, x_n]$.

Parameters:

ParameterTypeDescription
generatorslistList of generators of the ideal
elim_varsintNumber of variables to eliminate (from the front)
n_varsintTotal number of variables (default 1)
algorithmstrAlgorithm selection (default "auto")

Returns: a Gröbner basis of the elimination ideal.


MultivariatePolynomial (Gröbner helper class)

Already introduced in the Polynomials section. The Gröbner functions accept both MultivariatePolynomial and Polynomial as generators; the latter are automatically mapped to univariate multivariate polynomials.


GroebnerBasis

The result of a Gröbner basis computation.

Attributes:

AttributeTypeDescription
n_varsintNumber of variables

Methods:

MethodReturnsDescription
__len__()intNumber of polynomials in the basis
is_groebner_basis()boolVerifies whether it is indeed a Gröbner basis

RealSolution

A real solution of a polynomial system.

Attributes:

AttributeTypeDescription
valueslist[float]Coordinates of the solution
multiplicityintThe multiplicity

PolynomialSystemSolution

The solving result of a polynomial system.

Attributes:

AttributeTypeDescription
kindstr"zero_dimensional", "positive_dimensional", or "empty"
vector_space_dimensionint or NoneDimension of the vector space (only set for the zero-dimensional case)

Methods:

MethodReturnsDescription
solutions()list[RealSolution]List of real solutions (non-empty only in the zero-dimensional case)

HilbertSeries

The Hilbert series.

Attributes:

AttributeTypeDescription
dimensionintKrull dimension
degreeintMultiplicity (degree)
numeratorlist[int]Coefficients of the numerator polynomial

Methods:

MethodParametersReturnsDescription
hilbert_function(d)d: intintValue of the Hilbert function at degree $d$

PrimaryComponent

A component of a primary decomposition.

Attributes:

AttributeTypeDescription
n_varsintNumber of variables

Algebraic Numbers

AlgebraicExtension

The algebraic number field $K = \mathbb{Q}(\alpha)$, defined by a monic minimal polynomial.

Signature:

ocas.AlgebraicExtension(min_poly: list)

Parameters:

ParameterTypeDescription
min_polylist[int] or list[tuple[int,int]]Coefficients of the minimal polynomial (ascending powers); the last entry (leading coefficient) must be 1

Exceptions: ValueError — the coefficient list is too short, the leading coefficient is not 1, or irreducibility verification fails.

Example:

>>> from ocas import AlgebraicExtension
>>> # α² - 2 (i.e. ℚ(√2))
>>> field = AlgebraicExtension([-2, 0, 1])
>>> field.extension_degree()
2

AlgebraicExtension.extension_degree

AlgebraicExtension.extension_degree() -> int

Returns the degree of the extension $[K:\mathbb{Q}] = \deg(m)$.


AlgebraicExtension.alpha

AlgebraicExtension.alpha() -> AlgebraicElement

Returns the generator $\alpha$ of the extension.

Example:

>>> field = AlgebraicExtension([-2, 0, 1])
>>> a = field.alpha()
>>> a.coeffs()
['0', '1']

AlgebraicExtension.from_base

AlgebraicExtension.from_base(c) -> AlgebraicElement

Embeds the rational number $c$ as a constant element of the field.

Parameters:

ParameterTypeDescription
cint or tuple[int, int]A rational number (an integer or a (numerator, denominator) tuple)

AlgebraicExtension.element

AlgebraicExtension.element(coeffs: list) -> AlgebraicElement

Creates an element from the $\alpha$-polynomial coefficients (ascending powers).

Parameters:

ParameterTypeDescription
coeffslist[int] or list[tuple[int,int]]List of rational coefficients (ascending powers)

Example:

>>> field = AlgebraicExtension([-2, 0, 1])
>>> # 3 + 2α
>>> e = field.element([3, 2])
>>> e.coeffs()
['3', '2']

AlgebraicElement

An element of the algebraic number field $\mathbb{Q}(\alpha)$, stored as a polynomial in $\alpha$.

AlgebraicElement.coeffs

AlgebraicElement.coeffs() -> list[str]

Returns the $\alpha$-polynomial coefficients (list of strings, ascending powers).

Example:

>>> field = AlgebraicExtension([-2, 0, 1])
>>> e = field.element([1, 1])  # 1 + α = 1 + √2
>>> e.coeffs()
['1', '1']

AlgebraicPolynomial

Dense univariate polynomial over the algebraic number field $\mathbb{Q}(\alpha)$.

Signature:

ocas.AlgebraicPolynomial(field: AlgebraicExtension, coeffs: list)

Parameters:

ParameterTypeDescription
fieldAlgebraicExtensionThe algebraic number field containing the coefficients
coeffslistCoefficient list (ascending powers), each entry being an int, an (int, int) tuple, or an AlgebraicElement

Example:

>>> from ocas import AlgebraicExtension, AlgebraicPolynomial
>>> field = AlgebraicExtension([-2, 0, 1])
>>> # x² - 2 over ℚ(√2)
>>> p = AlgebraicPolynomial(field, [-2, 0, 1])
>>> p.degree()
2

AlgebraicPolynomial.degree

AlgebraicPolynomial.degree() -> int | None

Returns the degree. The zero polynomial returns None.


AlgebraicPolynomial.len

AlgebraicPolynomial.len() -> int

Returns the number of stored coefficients.


AlgebraicPolynomial.is_zero

AlgebraicPolynomial.is_zero() -> bool

Whether the polynomial is the zero polynomial.


AlgebraicPolynomial.coeffs

AlgebraicPolynomial.coeffs() -> list[list[str]]

Returns the coefficient list. Each coefficient is a list of rationals (as strings) representing the $\alpha$-polynomial coefficients.


AlgebraicPolynomial.factor

AlgebraicPolynomial.factor() -> list[AlgebraicFactor]

Factors the polynomial over the algebraic number field (Trager's algorithm).

Returns: a list of AlgebraicFactor, each containing an irreducible factor and its multiplicity.

Example:

>>> field = AlgebraicExtension([-2, 0, 1])
>>> p = AlgebraicPolynomial(field, [-2, 0, 1])  # x² - 2 = (x-α)(x+α)
>>> facs = p.factor()
>>> len(facs)
2
>>> [f.multiplicity for f in facs]
[1, 1]

AlgebraicFactor

A factor in the factorization of an algebraic-number-field polynomial.

Attributes:

AttributeTypeDescription
factorAlgebraicPolynomialThe irreducible factor
multiplicityintThe multiplicity

Automatic Differentiation

DualShape

First-order dual-number layout description, declaring the number of tracked differentiation variables. Construct once, share everywhere.

Signature:

# Not constructed directly; use the static method
shape = DualShape.first_order(n_vars)

DualShape.first_order (static method)

DualShape.first_order(n_vars: int) -> DualShape

Creates a layout with n_vars differentiation variables.

Parameters:

ParameterTypeDescription
n_varsintNumber of differentiation variables ($\geq 1$)

Exceptions: ValueError — n_vars == 0.

Example:

>>> from ocas import DualShape
>>> shape = DualShape.first_order(3)
>>> shape.n_vars
3
>>> shape.n_components
4

DualShape.n_vars

DualShape.n_vars -> int  # property

Number of differentiation variables.


DualShape.n_components

DualShape.n_components -> int  # property

Total number of components (value + derivative slots).


HyperDual

Hyper-dual number: a scalar value together with partial derivatives with respect to the variables of a DualShape. Computes exactly with rational numbers.

Limitations: supports only polynomial/rational arithmetic (+, -, *, /, negation). Transcendental functions (sin/exp/log) are not implemented; use repeated multiplication for integer powers.


HyperDual.variable (static method)

HyperDual.variable(shape: DualShape, i: int, value) -> HyperDual

Creates an independent variable $x_i = \text{value}$ (derivative 1 with respect to the $i$-th variable, 0 with respect to the others).

Parameters:

ParameterTypeDescription
shapeDualShapeThe dual-number layout
iintVariable index
valueint or tuple[int, int]The variable value (an integer or a rational tuple)

Exceptions: ValueError — index out of bounds.


HyperDual.constant (static method)

HyperDual.constant(shape: DualShape, value) -> HyperDual

Creates a constant (all derivatives zero).

Parameters:

ParameterTypeDescription
shapeDualShapeThe dual-number layout
valueint or tuple[int, int]The constant value

HyperDual.value

HyperDual.value() -> str

Returns the scalar value (as a string, e.g. "5" or "3/7").


HyperDual.deriv

HyperDual.deriv(i: int) -> str | None

Returns the partial derivative with respect to the $i$-th variable. Returns None if i is out of bounds.


HyperDual.n_vars

HyperDual.n_vars -> int  # property

Number of differentiation variables.


HyperDual arithmetic

OperationSyntaxDescription
Additiona + bComponent-wise addition
Subtractiona - bComponent-wise subtraction
Multiplicationa * bProduct rule
Divisiona / bQuotient rule
Negation-aComponent-wise negation

All operations require both operands to share the same DualShape. Different shapes raise ValueError.

Complete example:

>>> from ocas import DualShape, HyperDual
>>> shape = DualShape.first_order(2)
>>> x = HyperDual.variable(shape, 0, 3)
>>> y = HyperDual.variable(shape, 1, 5)
>>> f = x * y
>>> f.value()
'15'
>>> f.deriv(0)   # ∂f/∂x = y
'5'
>>> f.deriv(1)   # ∂f/∂y = x
'3'

Tensors

Tensor

Named tensor object with index slots and optional symmetry.

Signature:

ocas.Tensor(name: str, slots: list[tuple[str, str]], symmetry: str = "none")

Parameters:

ParameterTypeDescription
namestrTensor name
slotslist[tuple[str, str]]List of index slots, each being (label, position). Position is "upper" (contravariant) or "lower" (covariant)
symmetrystrSymmetry: "none" (default), "symmetric", or "antisymmetric"

Exceptions: ValueError — invalid position or symmetry string.

Example:

>>> from ocas import Tensor
>>> t = Tensor("T", [("i", "upper"), ("j", "lower")])
>>> t.name
'T'
>>> t.rank
2

Tensor.name

Tensor.name -> str  # property

The tensor name.


Tensor.rank

Tensor.rank -> int  # property

The tensor rank (number of slots).


Tensor.symmetry

Tensor.symmetry -> str  # property

The symmetry string ("none", "symmetric", "antisymmetric").


Tensor.slots

Tensor.slots() -> list[tuple[str, str]]

Returns the list of slots, each being (label, position).


Tensor.dummy_labels

Tensor.dummy_labels() -> list[str]

Returns the labels of the current dummy indices (those appearing twice).


Tensor.to_string_atom

Tensor.to_string_atom() -> str

Renders the tensor as the string form of an expression function node name(slot, slot, ...).


contract_tensors

ocas.contract_tensors(a: Tensor, b: Tensor) -> tuple[str, list | str]

Contracts two tensors: sums over shared dummy indices (same label, opposite variance).

Parameters:

ParameterTypeDescription
aTensorThe first tensor
bTensorThe second tensor

Returns: a (kind, payload) tuple:

  • when kind = "product", payload is the list of tensors after contraction (free indices concatenated)
  • when kind = "scalar", payload is the string of the contracted scalar expression

Example:

>>> from ocas import Tensor, contract_tensors
>>> t = Tensor("T", [("i", "upper"), ("j", "lower")])
>>> u = Tensor("U", [("j", "upper"), ("k", "lower")])
>>> kind, payload = contract_tensors(t, u)
>>> kind
'product'

tensor_symmetrise_sign

ocas.tensor_symmetrise_sign(tensor: Tensor) -> int

Returns the symmetrization sign of the tensor ($+1$ or $-1$). "none" and "symmetric" always return $+1$; "antisymmetric" returns the parity of the slot permutation.

Parameters:

ParameterTypeDescription
tensorTensorThe input tensor

Returns: $+1$ or $-1$.

Example:

>>> from ocas import Tensor, tensor_symmetrise_sign
>>> eps = Tensor("eps", [("a", "lower"), ("b", "lower")], symmetry="antisymmetric")
>>> tensor_symmetrise_sign(eps) in (1, -1)
True

canonicalize_tensors

ocas.canonicalize_tensors(expr: str, specs: dict[str, str], index_groups: dict[str, int] | None = None) -> str

Canonicalizes a tensor expression using the graph isomorphism engine.

Parameters:

ParameterTypeDescription
exprstrThe tensor expression string
specsdict[str, str]Tensor name → symmetry specification ("none", "symmetric", "antisymmetric")
index_groupsdict[str, int] or NoneIndex dimension groups (optional)

Returns: the canonicalized expression string.

Exceptions: ValueError — parsing or canonicalization failed.


young_project

ocas.young_project(expr: str, tableau: list[int]) -> str

Applies a Young projection to a tensor expression.

Parameters:

ParameterTypeDescription
exprstrThe tensor expression string
tableaulist[int]Row lengths of the Young diagram, e.g. [2, 1] denotes □□/□

Returns: the projected expression string.

Example:

>>> proj = ocas.young_project("f(a,b,c)", [1, 1, 1])  # □/□/□ fully antisymmetric projection
>>> "f(a,b,c)" in proj
True

refresh_dummies

ocas.refresh_dummies(expr: str, specs: dict[str, str]) -> str

Renames (refreshes) the dummy indices in a tensor expression.

Parameters:

ParameterTypeDescription
exprstrThe tensor expression string
specsdict[str, str]Tensor name → symmetry specification

Returns: the expression string with the dummy indices renamed.


Evaluation

ExpressionEvaluator

Compiles an expression for fast numerical evaluation. Compile once, evaluate many times.

Signature:

ocas.ExpressionEvaluator(input: str, param_names: list[str])

Parameters:

ParameterTypeDescription
inputstrThe expression string
param_nameslist[str]List of parameter names (in evaluation order)

Exceptions: ValueError — parsing or compilation error.

Example:

>>> from ocas import ExpressionEvaluator
>>> ev = ExpressionEvaluator("x^2 + y", ["x", "y"])
>>> ev.evaluate([3.0, 1.0])
[10.0]
>>> ev.evaluate([2.0, 0.0])
[4.0]

ExpressionEvaluator.evaluate

ExpressionEvaluator.evaluate(values: list[float]) -> list[float]

Evaluates with the given parameter values.

Parameters:

ParameterTypeDescription
valueslist[float]List of floating-point values; its length must match the number of parameters

Returns: the list of result floats.

Exceptions: ValueError — parameter count mismatch or evaluation error.


ExpressionEvaluator.n_params

ExpressionEvaluator.n_params -> int  # property

The number of parameters.


Numerical Integration

Vegas

Adaptive Monte Carlo integrator (the Vegas algorithm), supporting multi-dimensional integrals.

Signature:

ocas.Vegas(n_dims: int, *, n_bins=None, n_samples=None, iterations=None, learning_rate=None, seed=None)

Parameters:

ParameterTypeDescription
n_dimsintThe integration dimension
n_binsint or NoneNumber of bins per dimension (default 64)
n_samplesint or NoneNumber of samples per iteration (default 10000)
iterationsint or NoneNumber of iterations (default 10)
learning_ratefloat or NoneGrid adaptation learning rate (default 1.5)
seedint or NoneRNG seed (default 0x0C45)

Example:

>>> from ocas import Vegas
>>> v = Vegas(2, n_samples=20000, iterations=8, seed=1)
>>> r = v.integrate(lambda xs: xs[0] * xs[1])
>>> abs(r.integral - 0.25) < 0.01
True

Vegas.integrate

Vegas.integrate(f: Callable[[list[float]], float]) -> IntegrateResult

Integrates the callable $f$ over the unit hypercube $[0, 1]^n$.

Parameters:

ParameterTypeDescription
fCallableAccepts a list of n_dims floats in [0, 1] and returns a single float

Returns: an IntegrateResult.


Vegas.result

Vegas.result -> IntegrateResult  # property

The latest accumulated estimate and error.


Vegas.iterations

Vegas.iterations -> int  # property

The number of completed iterations.


integrate_1d

ocas.integrate_1d(f: Callable[[float], float], a: float, b: float, *, n_bins=None, n_samples=None, iterations=None, learning_rate=None, seed=None) -> IntegrateResult

Convenience function for one-dimensional numerical integration.

Parameters:

ParameterTypeDescription
fCallableA unary floating-point function
afloatLower integration limit
bfloatUpper integration limit
n_bins etc.Same as VegasOptional tuning parameters

Returns: an IntegrateResult.

Example:

>>> import ocas
>>> r = ocas.integrate_1d(lambda x: x, 0.0, 1.0)
>>> abs(r.integral - 0.5) < 0.01
True

IntegrateResult

The result of a numerical integration.

Attributes:

AttributeTypeDescription
integralfloatThe integral estimate
errorfloatThe estimated standard error

Supports indexing: result[0] = integral, result[1] = error; supports unpacking: integral, error = result.

Example:

>>> import ocas
>>> r = ocas.integrate_1d(lambda x: x**2, 0.0, 1.0, seed=42)
>>> r[0]       # integral
0.333...
>>> r[1]       # error
0.00...
>>> i, e = r   # unpacking

Double-Precision Floats

DoubleF64

Double-precision floating-point arithmetic (~31 significant decimal digits, ~84 binary bits), using the Dekker/Knuth "double-float" algorithm.

Signature:

ocas.DoubleF64(hi: float, lo: float = 0.0)

Parameters:

ParameterTypeDescription
hifloatThe primary value
lofloatThe error term (default 0.0)

Example:

>>> from ocas import DoubleF64
>>> a = DoubleF64(1.0)
>>> b = DoubleF64(2.0)
>>> str(a + b)
'3'

DoubleF64.to_f64

DoubleF64.to_f64() -> float

Converts to a standard float (loses the precision of the error term).


DoubleF64.components

DoubleF64.components() -> tuple[float, float]

Returns the (hi, lo) tuple.


DoubleF64 arithmetic

OperationSyntaxDescription
Additiona + bDouble-precision addition
Subtractiona - bDouble-precision subtraction
Multiplicationa * bDouble-precision multiplication
Divisiona / bDouble-precision division
Negation-aComponent-wise negation
Absolute valueabs(a)Double-precision absolute value
Powera ** nInteger power (n is an int)
Comparison==, <, <=, >, >=Total order comparison

Exceptions: ValueError — division by zero (__truediv__).


DoubleF64 transcendental functions

MethodDescriptionExceptions
sin()Sine—
cos()Cosine—
tan()Tangent—
exp()Natural exponential $e^x$—
ln()Natural logarithmValueError: non-positive input
sqrt()Square rootValueError: negative input

Complete example:

>>> from ocas import DoubleF64
>>> DoubleF64(0.0).sin().to_f64()
0.0
>>> DoubleF64(4.0).sqrt().components()
(2.0, 0.0)

Function Quick Reference

FunctionSignatureDescription
solve_linear_rational(a, b) -> list[tuple]Solves Ax=b over ℚ
solve_linear_integer(a, b) -> list[int]Solves Ax=b over ℤ
solve_diophantine(a, b, c) -> DiophantineSolution | Noneax+by=c
classify_ode(equation, func, var) -> list[str]ODE classification
dsolve(equation, func, var, hint=None) -> strSymbolically solves ODEs
dsolve_ivp(equation, func, var, y0, y1=None) -> strLaplace IVP
factorint(n) -> list[tuple]Integer prime factorization
isprime(n) -> boolBPSW primality test
isprime_u64(n) -> boolDeterministic primality (u64)
nextprime(n) -> intNext prime
discrete_log(p, base, target) -> intDiscrete logarithm
crt(moduli, residues) -> tupleChinese remainder theorem
jacobi_symbol(a, n) -> intJacobi symbol
totient(n) -> intEuler totient
mobius(n) -> intMöbius function
divisor_count(n) -> intNumber of divisors
divisor_sigma(n, k=1) -> intSum of divisor powers
liouville_lambda(n) -> intLiouville function
py_groebner_basis(generators, n_vars=1, algorithm="auto")Gröbner basis
py_ideal_contains(generators, f, n_vars=1, algorithm="auto")Ideal membership test
py_solve_polynomial_system(equations, n_vars=1, algorithm="auto")Solves polynomial systems
py_hilbert_series(gb) -> HilbertSeriesHilbert series
py_ideal_radical(generators, n_vars=1)Ideal radical
py_primary_decomposition(generators, n_vars=1)Primary decomposition
py_is_zero_dimensional(gb) -> boolZero-dimensionality test
py_eliminate(generators, elim_vars, n_vars=1, algorithm="auto")Elimination
contract_tensors(a, b) -> tupleTensor contraction
tensor_symmetrise_sign(tensor) -> intSymmetrization sign
canonicalize_tensors(expr, specs, index_groups=None)Tensor canonicalization
young_project(expr, tableau) -> strYoung projection
refresh_dummies(expr, specs) -> strDummy index refresh
integrate_1d(f, a, b, **opts) -> IntegrateResultOne-dimensional numerical integration

Class Quick Reference

ClassConstructorDescription
ExpressionExpression(input)Symbolic expression
PolynomialPolynomial(coeffs, domain=None)Univariate polynomial
MultivariatePolynomialMultivariatePolynomial(terms, n_vars)Multivariate polynomial
MatrixMatrix(rows, domain=None)Matrix over a domain
IntegerDomainIntegerDomain()ℤ selector
RationalDomainRationalDomain()ℚ selector
FiniteFieldFiniteField(modulus)GF(p)
AlgebraicExtensionAlgebraicExtension(min_poly)Algebraic number field
AlgebraicElement(created by AlgebraicExtension)Field element
AlgebraicPolynomialAlgebraicPolynomial(field, coeffs)Polynomial over an algebraic field
DualShapeDualShape.first_order(n_vars)Dual-number layout
HyperDualHyperDual.variable(...) / .constant(...)Hyper-dual number
TensorTensor(name, slots, symmetry="none")Named tensor
ExpressionEvaluatorExpressionEvaluator(input, param_names)Numerical evaluator
VegasVegas(n_dims, **opts)Monte Carlo integrator
IntegrateResultIntegrateResult(integral, error)Integration result
DoubleF64DoubleF64(hi, lo=0.0)Double-precision float
GroebnerBasis(returned by functions)Gröbner basis result
RealSolution(returned by functions)Real solution
PolynomialSystemSolution(returned by functions)System solving result
HilbertSeries(returned by functions)Hilbert series
PrimaryComponent(returned by functions)Primary decomposition component
DiophantineSolution(returned by functions)Diophantine equation solution
PolynomialFactor(returned by functions)Factorization factor
AlgebraicFactor(returned by functions)Algebraic-field factorization factor

C/C++ API Reference

⚠️ Important note: the ocas.h header is out of date

The cbindgen-generated ocas-c/include/ocas.h is missing the following 9 functions:

  • ocas_expr_integrate_heuristic (expression domain)
  • 8 Gröbner-domain exports (ocas_groebner_basis, ocas_groebner_basis_free, ocas_groebner_basis_len, ocas_is_zero_dimensional, ocas_solve_polynomial_system, ocas_system_solution_count, ocas_system_solution_value, ocas_system_solution_free)

This document is based on the actual exports in ocas-c/src/*.rs, not the header file. When using the missing functions you must declare the prototypes yourself.

⚠️ Gröbner module caveats

The Gröbner module uses a non-standard -1 error code (instead of the OCAS_* constants), and the OcasGroebnerBasis and OcasSystemSolution handles are not marked #[repr(C)] — they work correctly in the current Rust version, but ABI stability is not guaranteed by the compiler.

⚠️ C++ wrapper coverage

ocas.hpp covers only three domains (22 wrapper functions in total):

  • Expressions: the ocas::Expression class (8 members: 2 constructors + 6 methods)
  • Number theory: ocas::ntheory::* (11 inline functions)
  • Tensors: ocas::tensor::* (3 inline functions)

The remaining 69 exports have no C++ RAII wrappers and must be called through the C API directly.


General Conventions

Error Handling

All fallible functions follow this pattern:

int err = 0;
OcasExpr *e = ocas_expr_parse("x^2 + 1", &err);
if (e == NULL) {
    fprintf(stderr, "error %d: %s\n", err, ocas_error_last_message());
    ocas_error_clear();
    return 1;
}
  • The err_out parameter may be NULL (error code ignored).
  • Functions returning pointers: NULL on failure.
  • Functions returning int: non-zero on failure (see each function's documentation for the exact meaning).
  • Functions returning size_t: 0 on failure.
  • Number theory functions returning int use the special sentinel values -1 (isprime) or -2 (jacobi/mobius/liouville) to signal errors.

Memory Management

Return typeFree function
char* (string)ocas_string_free()
OcasExpr*ocas_expr_free()
OcasPolyZ*ocas_poly_z_free()
OcasPolyFp*ocas_poly_fp_free()
OcasPolyFactorArrayocas_poly_factor_array_free() (frees only the array structure, not the internal polynomial handles)
OcasAlgebraicField*ocas_algebraic_field_free()
OcasAlgebraicPoly*ocas_algebraic_poly_free()
OcasAlgebraicFactorArrayocas_algebraic_factor_array_free() (as above, frees only the array)
OcasVegas*ocas_vegas_free()
OcasTensor*ocas_tensor_free()
OcasTensorContractionocas_tensor_contraction_free() (frees arrays/strings, not the internal tensor handles)
OcasDualShape*ocas_dual_shape_free()
OcasHyperDual*ocas_hyperdual_free()
OcasGroebnerBasis*ocas_groebner_basis_free()
OcasSystemSolution*ocas_system_solution_free()

All *_free functions are safe on NULL arguments (no-op).


1. Errors/Utilities (3 functions)

ocas_version

Signature:

const char *ocas_version(void);

Description: Returns the oCAS version string.

Return value: A static string pointer valid for the lifetime of the program. The caller must not free or modify it.

Example:

printf("oCAS version: %s\n", ocas_version());
// Output: oCAS version: 0.24.x

ocas_error_last_message

Signature:

const char *ocas_error_last_message(void);

Description: Returns the message of the last error on the calling thread, or NULL if there was no error.

Return value: A library-owned string pointer. Must not be freed or modified. It becomes invalid after calling any error-setting function or ocas_error_clear() on the same thread.

Example:

ocas_expr_parse("invalid !!!", NULL);
const char *msg = ocas_error_last_message();
if (msg) printf("error: %s\n", msg);
ocas_error_clear();

ocas_error_clear

Signature:

void ocas_error_clear(void);

Description: Clears the last-error state on the calling thread.

Example:

ocas_error_clear(); // reset the error state

2. Expressions (12 functions)

Expressions are the core type of oCAS. Each OcasExpr* handle owns a private arena, an AtomArena, and a normalized root Atom. Handles are movable; the heap address does not change over the handle's lifetime.

ocas_expr_parse

Signature:

OcasExpr *ocas_expr_parse(const char *input, int *err_out);

Description: Parses an input string into a new expression handle.

Parameters:

ParameterTypeDescription
inputconst char*Null-terminated expression string, e.g. "x^2 + 2*x + 1"
err_outint*May be NULL; when non-NULL, the error code is written here

Return value: OcasExpr* on success; NULL on failure (OCAS_ERROR_PARSE).

Memory: The caller is responsible for freeing with ocas_expr_free().

Example:

int err = 0;
OcasExpr *e = ocas_expr_parse("sin(x)^2 + cos(x)^2", &err);
assert(err == OCAS_OK && e != NULL);
ocas_expr_free(e);

ocas_expr_free

Signature:

void ocas_expr_free(OcasExpr *handle);

Description: Frees an expression handle. NULL-safe (no-op).

Parameters:

ParameterTypeDescription
handleOcasExpr*Handle returned by ocas_expr_parse or other expression functions

Example:

ocas_expr_free(e);     // free
ocas_expr_free(NULL);  // no-op

ocas_expr_clone

Signature:

OcasExpr *ocas_expr_clone(const OcasExpr *handle, int *err_out);

Description: Clones the expression into a new arena. Returns an independent copy.

Parameters:

ParameterTypeDescription
handleconst OcasExpr*Non-NULL expression handle
err_outint*May be NULL

Return value: A new handle or NULL (OCAS_ERROR_RUNTIME).

Example:

OcasExpr *copy = ocas_expr_clone(original, NULL);
// copy and original are independent; they do not affect each other
ocas_expr_free(copy);

ocas_expr_to_string

Signature:

char *ocas_expr_to_string(const OcasExpr *handle, int *err_out);

Description: Renders the expression as a null-terminated C string.

Parameters:

ParameterTypeDescription
handleconst OcasExpr*Non-NULL expression handle
err_outint*May be NULL

Return value: A heap-allocated string (free with ocas_string_free()), or NULL.

Example:

char *s = ocas_expr_to_string(e, NULL);
printf("expression: %s\n", s);
ocas_string_free(s);

ocas_string_free

Signature:

void ocas_string_free(char *s);

Description: Frees a string allocated by ocas_expr_to_string or another function returning char*. NULL-safe.

Parameters:

ParameterTypeDescription
schar*Heap-allocated string pointer

ocas_expr_normalize

Signature:

int ocas_expr_normalize(OcasExpr *handle, int *err_out);

Description: Re-normalizes the expression handle in place.

Parameters:

ParameterTypeDescription
handleOcasExpr*Non-NULL expression handle (modified)
err_outint*May be NULL

Return value: OCAS_OK (0) on success; non-zero on failure.


ocas_expr_diff

Signature:

OcasExpr *ocas_expr_diff(const OcasExpr *handle, const char *var, int *err_out);

Description: Differentiates the expression with respect to the variable var. Returns a new handle.

Parameters:

ParameterTypeDescription
handleconst OcasExpr*Non-NULL expression handle
varconst char*Variable name, e.g. "x"
err_outint*May be NULL

Return value: The derivative expression handle or NULL.

Example:

OcasExpr *e = ocas_expr_parse("x^3 + 2*x", NULL);
OcasExpr *de = ocas_expr_diff(e, "x", NULL);
char *s = ocas_expr_to_string(de, NULL);
printf("d/dx = %s\n", s);  // Output: 2 + (3*(x^2))
ocas_string_free(s);
ocas_expr_free(de);
ocas_expr_free(e);

ocas_expr_integrate

Signature:

OcasExpr *ocas_expr_integrate(const OcasExpr *handle, const char *var, int *err_out);

Description: Integrates the expression with respect to the variable var. If no symbolic solution is found, returns the unevaluated form Integral(expr, var). The result is not certified; use ocas_expr_integrate_outcome when a machine-checkable certificate is required.

Parameters: Same as ocas_expr_diff.

Return value: The integral expression handle or NULL.

Example:

OcasExpr *e = ocas_expr_parse("3*x^2", NULL);
OcasExpr *ie = ocas_expr_integrate(e, "x", NULL);
char *s = ocas_expr_to_string(ie, NULL);
printf("∫ = %s\n", s);  // Output: 3*(3^-1)*(x^3)
ocas_string_free(s);
ocas_expr_free(ie);
ocas_expr_free(e);

ocas_expr_integrate_outcome

Signature:

OcasExpr *ocas_expr_integrate_outcome(const OcasExpr *handle, const char *var,
                                      int *outcome_out, int *err_out);

Description: Certified integration (new in 0.28.0). *outcome_out is one of ocas_OCAS_INTEGRATION_FOUND, ocas_OCAS_INTEGRATION_PROVED_NONELEMENTARY or ocas_OCAS_INTEGRATION_UNKNOWN. A FOUND result always carries a machine-checkable symbolic certificate (D(F) − f ≡ 0); UNKNOWN honestly returns the unevaluated Integral(expr, var) form; a result the exact checker falsifies is never returned as FOUND. PROVED_NONELEMENTARY has no producer in 0.28.0 (reserved for the non-elementary layer).

Parameters:

ParameterDescription
handleThe integrand expression
varThe integration variable name
outcome_outOutput for the three-valued verdict; may be NULL
err_outError code output

Return value: A new expression handle (caller owns it) or NULL.

Example:

int outcome = -1;
OcasExpr *ie = ocas_expr_integrate_outcome(e, "x", &outcome, NULL);
if (outcome == ocas_OCAS_INTEGRATION_FOUND) {
    /* ie carries a certificate: D(ie) == integrand */
}
ocas_expr_free(ie);

ocas_expr_integrate_heuristic

Signature:

OcasExpr *ocas_expr_integrate_heuristic(const OcasExpr *handle, const char *var, int *err_out);

Description: Integrates using heuristic techniques (integration by parts, trigonometric substitution, Weierstrass rationalization, Euler substitution). If no heuristic succeeds, returns the unevaluated form.

⚠️ Note: This function is not included in the ocas.h header. Declare the prototype yourself:

extern OcasExpr *ocas_expr_integrate_heuristic(const OcasExpr *handle,
                                                const char *var,
                                                int *err_out);

Parameters: Same as ocas_expr_diff.

Return value: The integral expression handle or NULL.


ocas_expr_taylor

Signature:

OcasExpr *ocas_expr_taylor(const OcasExpr *handle, const char *var,
                           const OcasExpr *point, uint32_t order, int *err_out);

Description: Computes the Taylor expansion of the expression in var of order order at the point point.

Parameters:

ParameterTypeDescription
handleconst OcasExpr*Non-NULL expression handle
varconst char*Expansion variable
pointconst OcasExpr*Expansion point (expression handle)
orderuint32_tExpansion order
err_outint*May be NULL

Return value: The Taylor expansion handle or NULL.

Example:

OcasExpr *f = ocas_expr_parse("exp(x)", NULL);
OcasExpr *zero = ocas_expr_parse("0", NULL);
OcasExpr *t = ocas_expr_taylor(f, "x", zero, 4, NULL);
char *s = ocas_expr_to_string(t, NULL);
printf("Taylor = %s\n", s);  // Output: 1 + x + ((2^-1)*(x^2)) + ((6^-1)*(x^3)) + ((24^-1)*(x^4))
ocas_string_free(s);
ocas_expr_free(t);
ocas_expr_free(zero);
ocas_expr_free(f);

ocas_expr_simplify

Signature:

OcasExpr *ocas_expr_simplify(const OcasExpr *handle, int *err_out);

Description: Simplifies the expression using the default rule set. Returns a new handle.

Parameters:

ParameterTypeDescription
handleconst OcasExpr*Non-NULL expression handle
err_outint*May be NULL

Return value: The simplified expression handle or NULL.

Example:

OcasExpr *e = ocas_expr_parse("x + x", NULL);
OcasExpr *simplified = ocas_expr_simplify(e, NULL);
char *s = ocas_expr_to_string(simplified, NULL);
printf("simplified = %s\n", s);  // Output: 2*x
ocas_string_free(s);
ocas_expr_free(simplified);
ocas_expr_free(e);

ocas_expr_substitute

Signature:

OcasExpr *ocas_expr_substitute(const OcasExpr *handle, const char *var,
                                const OcasExpr *replacement, int *err_out);

Description: Replaces every occurrence of var in the expression with replacement.

Parameters:

ParameterTypeDescription
handleconst OcasExpr*Non-NULL expression handle
varconst char*Name of the variable to replace
replacementconst OcasExpr*Replacement expression (non-NULL)
err_outint*May be NULL

Return value: The substituted expression handle or NULL.

Example:

OcasExpr *e = ocas_expr_parse("x^2 + y", NULL);
OcasExpr *val = ocas_expr_parse("3", NULL);
OcasExpr *result = ocas_expr_substitute(e, "x", val, NULL);
char *s = ocas_expr_to_string(result, NULL);
printf("substituted = %s\n", s);  // Output: y + (3^2)
ocas_string_free(s);
ocas_expr_free(result);
ocas_expr_free(val);
ocas_expr_free(e);

C++ wrapper: ocas::Expression

ocas.hpp provides a RAII wrapper class that manages handle lifetimes automatically:

#include <ocas.h>
#include <ocas.hpp>

try {
    ocas::Expression e("x^2 + 1");
    ocas::Expression d = e.diff("x");
    std::cout << "d/dx = " << d.to_string() << std::endl;
    // all handles freed automatically
} catch (const ocas::Error& ex) {
    std::cerr << "error: " << ex.what() << std::endl;
}
C++ methodCorresponding C function
Expression(input)ocas_expr_parse
Expression(other)ocas_expr_clone
to_string()ocas_expr_to_string + ocas_string_free
diff(var)ocas_expr_diff
integrate(var)ocas_expr_integrate
simplify()ocas_expr_simplify
substitute(var, rep)ocas_expr_substitute
raw()Get the raw OcasExpr* (does not transfer ownership)

3. Integer Polynomials (6 functions)

Handles bivariate (x, y) integer polynomials ℤ[x,y]. Parsed from strings; factorization supported.

ocas_poly_z_create

Signature:

OcasPolyZ *ocas_poly_z_create(const char *input, int *err);

Description: Creates a bivariate integer polynomial from a string. The input may contain the variables x, y, integer coefficients, addition, multiplication, and non-negative integer powers.

Parameters:

ParameterTypeDescription
inputconst char*Polynomial string, e.g. "x^2*y + 3*x - 1"
errint*May be NULL

Return value: OcasPolyZ* handle or NULL (OCAS_ERROR_PARSE).

Memory: The caller frees with ocas_poly_z_free().

Example:

int err = 0;
OcasPolyZ *p = ocas_poly_z_create("x^2 + y^2 - 1", &err);
assert(p != NULL);
ocas_poly_z_free(p);

ocas_poly_z_free

Signature:

void ocas_poly_z_free(OcasPolyZ *poly);

Description: Frees an integer polynomial handle. NULL-safe.


ocas_poly_z_clone

Signature:

OcasPolyZ *ocas_poly_z_clone(const OcasPolyZ *poly);

Description: Clones an integer polynomial. Returns a new handle (caller frees it).

Return value: A new OcasPolyZ* or NULL (NULL input or out of memory).


ocas_poly_z_degree

Signature:

size_t ocas_poly_z_degree(const OcasPolyZ *poly);

Description: Returns the total degree of the polynomial. The zero polynomial returns 0. A NULL handle returns 0.


ocas_poly_z_to_string

Signature:

char *ocas_poly_z_to_string(const OcasPolyZ *poly, int *err);

Description: Returns a heap-allocated string representation of the polynomial. Free with ocas_string_free().

Return value: The string or NULL.

Example:

char *s = ocas_poly_z_to_string(p, NULL);
printf("polynomial: %s\n", s);
ocas_string_free(s);

ocas_poly_z_factor

Signature:

int ocas_poly_z_factor(const OcasPolyZ *poly, OcasPolyFactorArray *out, int *err);

Description: Factors a bivariate integer polynomial.

Parameters:

ParameterTypeDescription
polyconst OcasPolyZ*Non-NULL polynomial handle
outOcasPolyFactorArray*Output struct (filled on success)
errint*May be NULL

Return value: OCAS_OK (0) on success; non-zero on failure (out unchanged).

Memory: On success, out.factors[i].poly is a void* that must be cast to OcasPolyZ* and freed with ocas_poly_z_free(). The array itself is freed with ocas_poly_factor_array_free().

Example:

OcasPolyZ *p = ocas_poly_z_create("x^2 - y^2", NULL);
OcasPolyFactorArray arr = {0};
if (ocas_poly_z_factor(p, &arr, NULL) == OCAS_OK) {
    for (size_t i = 0; i < arr.len; i++) {
        OcasPolyZ *f = (OcasPolyZ *)arr.factors[i].poly;
        char *s = ocas_poly_z_to_string(f, NULL);
        printf("factor: %s (multiplicity %zu)\n", s, arr.factors[i].multiplicity);
        ocas_string_free(s);
        ocas_poly_z_free(f);
    }
    ocas_poly_factor_array_free(&arr);
}
ocas_poly_z_free(p);

4. Finite Field Polynomials (6 functions)

Handles bivariate polynomials $𝔽_p[x,y]$.

ocas_poly_fp_create

Signature:

OcasPolyFp *ocas_poly_fp_create(const char *input, const char *prime, int *err);

Description: Creates a bivariate polynomial over the prime field $𝔽_p$. Coefficients in the string are automatically reduced modulo $p$.

Parameters:

ParameterTypeDescription
inputconst char*Polynomial string
primeconst char*Decimal string of the prime $p$
errint*May be NULL

Return value: OcasPolyFp* handle or NULL.

Memory: The caller frees with ocas_poly_fp_free().

Example:

OcasPolyFp *p = ocas_poly_fp_create("x^2 + 2*x + 1", "7", NULL);
// coefficients in 𝔽₇: x² + 2x + 1
ocas_poly_fp_free(p);

ocas_poly_fp_free

Signature:

void ocas_poly_fp_free(OcasPolyFp *poly);

Description: Frees a finite field polynomial handle. NULL-safe.


ocas_poly_fp_clone

Signature:

OcasPolyFp *ocas_poly_fp_clone(const OcasPolyFp *poly);

Description: Clones a finite field polynomial. Returns a new handle.


ocas_poly_fp_degree

Signature:

size_t ocas_poly_fp_degree(const OcasPolyFp *poly);

Description: Returns the total degree of the polynomial. NULL returns 0.


ocas_poly_fp_to_string

Signature:

char *ocas_poly_fp_to_string(const OcasPolyFp *poly, int *err);

Description: Returns a heap-allocated string of the polynomial. Free with ocas_string_free().


ocas_poly_fp_factor

Signature:

int ocas_poly_fp_factor(const OcasPolyFp *poly, OcasPolyFactorArray *out, int *err);

Description: Factors a finite field polynomial. Return value and memory management are the same as ocas_poly_z_factor, except that out.factors[i].poly must be cast to OcasPolyFp*.

Example:

OcasPolyFp *p = ocas_poly_fp_create("x^2 - 1", "5", NULL);
OcasPolyFactorArray arr = {0};
if (ocas_poly_fp_factor(p, &arr, NULL) == OCAS_OK) {
    for (size_t i = 0; i < arr.len; i++) {
        OcasPolyFp *f = (OcasPolyFp *)arr.factors[i].poly;
        char *s = ocas_poly_fp_to_string(f, NULL);
        printf("factor: %s (multiplicity %zu)\n", s, arr.factors[i].multiplicity);
        ocas_string_free(s);
        ocas_poly_fp_free(f);
    }
    ocas_poly_factor_array_free(&arr);
}
ocas_poly_fp_free(p);

5. Factor Arrays (1 function)

ocas_poly_factor_array_free

Signature:

void ocas_poly_factor_array_free(OcasPolyFactorArray *arr);

Description: Frees the factor array structure itself and the internal OcasPolyFactor objects, but does not free the polynomial handles of the individual factors.

⚠️ Memory pitfall: you must free each arr->factors[i].poly with ocas_poly_z_free() / ocas_poly_fp_free() before calling this function. Doing it in the wrong order causes a use-after-free.

Parameters:

ParameterTypeDescription
arrOcasPolyFactorArray*Array filled by ocas_poly_z_factor or ocas_poly_fp_factor

6. Algebraic Number Fields (9 functions)

Handles polynomials over algebraic number fields $ℚ(α)$. Supports the Trager factorization algorithm.

Data Formats

Minimal polynomial: a univariate string; the variable must be x, e.g. "x^2 - 2" denotes $ℚ(\sqrt{2})$.

Coefficient lists: semicolon-separated (constant term first); each coefficient is a comma-separated list of rationals (an $α$-polynomial, ascending order). For example, over $ℚ(\sqrt{2})$:

  • "-2;0;1" — $x^2 - 2$ (all coefficients in the base field)
  • "0;0;0,1" — $x^2 - α$ (the $x^2$ coefficient is $0 + 1 \cdot α$)

A single rational may omit the comma.

ocas_algebraic_field_create

Signature:

OcasAlgebraicField *ocas_algebraic_field_create(const char *min_poly, int *err);

Description: Creates an algebraic number field from a monic minimal polynomial.

Parameters:

ParameterTypeDescription
min_polyconst char*Minimal polynomial string, e.g. "x^2 - 2"
errint*May be NULL

Return value: OcasAlgebraicField* handle or NULL.

Memory: The caller frees with ocas_algebraic_field_free().

Example:

OcasAlgebraicField *q_sqrt2 = ocas_algebraic_field_create("x^2 - 2", NULL);
printf("extension degree: %zu\n", ocas_algebraic_field_degree(q_sqrt2));
// Output: extension degree: 2
ocas_algebraic_field_free(q_sqrt2);

ocas_algebraic_field_free

Signature:

void ocas_algebraic_field_free(OcasAlgebraicField *field);

Description: Frees an algebraic number field handle. NULL-safe.


ocas_algebraic_field_degree

Signature:

size_t ocas_algebraic_field_degree(const OcasAlgebraicField *field);

Description: Returns the extension degree $\deg(m) = [ℚ(α):ℚ]$. NULL returns 0.


ocas_algebraic_poly_create

Signature:

OcasAlgebraicPoly *ocas_algebraic_poly_create(const OcasAlgebraicField *field,
                                              const char *coeffs, int *err);

Description: Creates a polynomial over the algebraic number field.

Parameters:

ParameterTypeDescription
fieldconst OcasAlgebraicField*Non-NULL field handle
coeffsconst char*Coefficient list string (see format above)
errint*May be NULL

Return value: OcasAlgebraicPoly* handle or NULL.

Example:

OcasAlgebraicField *fld = ocas_algebraic_field_create("x^2 - 2", NULL);
// Create the polynomial x² - α (i.e. x² - √2)
OcasAlgebraicPoly *p = ocas_algebraic_poly_create(fld, "0;0;0,1", NULL);
printf("degree: %zu\n", ocas_algebraic_poly_degree(p));
ocas_algebraic_poly_free(p);
ocas_algebraic_field_free(fld);

ocas_algebraic_poly_free

Signature:

void ocas_algebraic_poly_free(OcasAlgebraicPoly *poly);

Description: Frees an algebraic field polynomial handle. NULL-safe.


ocas_algebraic_poly_degree

Signature:

size_t ocas_algebraic_poly_degree(const OcasAlgebraicPoly *poly);

Description: Returns the degree of the polynomial. The zero polynomial returns 0.


ocas_algebraic_poly_to_string

Signature:

char *ocas_algebraic_poly_to_string(const OcasAlgebraicPoly *poly, int *err);

Description: Returns a heap-allocated string of the polynomial. The format is [c0] + [c1]*x + [c2]*x^2 + ..., where each [ci] is a comma-separated list of $α$-polynomial rationals. Free with ocas_string_free().


ocas_algebraic_poly_factor

Signature:

int ocas_algebraic_poly_factor(const OcasAlgebraicPoly *poly,
                               OcasAlgebraicFactorArray *out, int *err);

Description: Factors a polynomial over an algebraic number field using the Trager algorithm.

Parameters:

ParameterTypeDescription
polyconst OcasAlgebraicPoly*Non-NULL polynomial handle
outOcasAlgebraicFactorArray*Output array (filled on success)
errint*May be NULL

Return value: OCAS_OK (0) on success.

Memory: out->factors[i].poly (a void*) must be cast to OcasAlgebraicPoly* and freed with ocas_algebraic_poly_free(). The array is freed with ocas_algebraic_factor_array_free().

Example:

OcasAlgebraicField *fld = ocas_algebraic_field_create("x^2 - 2", NULL);
OcasAlgebraicPoly *p = ocas_algebraic_poly_create(fld, "-2;0;1", NULL);
OcasAlgebraicFactorArray arr = {0};
if (ocas_algebraic_poly_factor(p, &arr, NULL) == OCAS_OK) {
    for (size_t i = 0; i < arr.len; i++) {
        OcasAlgebraicPoly *f = (OcasAlgebraicPoly *)arr.factors[i].poly;
        char *s = ocas_algebraic_poly_to_string(f, NULL);
        printf("factor: %s (multiplicity %zu)\n", s, arr.factors[i].multiplicity);
        ocas_string_free(s);
        ocas_algebraic_poly_free(f);
    }
    ocas_algebraic_factor_array_free(&arr);
}
ocas_algebraic_poly_free(p);
ocas_algebraic_field_free(fld);

ocas_algebraic_factor_array_free

Signature:

void ocas_algebraic_factor_array_free(OcasAlgebraicFactorArray *arr);

Description: Frees the storage of the factor array. Does not free the polynomial handles of the individual factors — free those separately first.


7. Numeric Integration (6 functions)

Vegas adaptive Monte Carlo integrator.

Integrand function signature

typedef double (*ocas_integrand_t)(double x, void *user_data);

user_data is passed through from the caller to the integrand unchanged.

⚠️ Note: the current C API passes only the first coordinate of the sampled point to the integrand (a 1-D-friendly interface). Even if the integrator is created with n_dims > 1, the x argument of the integrand represents the first dimension only; a full multi-dimensional const double*/size_t signature is planned for a future release.

ocas_vegas_create

Signature:

OcasVegas *ocas_vegas_create(size_t n_dims, const OcasVegasOptions *opts, int *err);

Description: Creates an n_dims-dimensional Vegas integrator.

Parameters:

ParameterTypeDescription
n_dimssize_tNumber of integration dimensions
optsconst OcasVegasOptions*May be NULL (uses defaults)
errint*May be NULL

OcasVegasOptions fields:

FieldTypeDefaultDescription
n_binssize_t64Number of bins per dimension
n_samplessize_t10000Samples per iteration
iterationssize_t10Number of adaptive iterations
learning_ratedouble1.5Grid smoothing/learning rate
seeduint64_t0x0C45Random number seed

Return value: OcasVegas* handle or NULL.

Memory: The caller frees with ocas_vegas_free().

Example:

OcasVegasOptions opts = {
    .n_bins = 50, .n_samples = 20000, .iterations = 8,
    .learning_rate = 1.5, .seed = 42
};
int err = 0;
OcasVegas *v = ocas_vegas_create(1, &opts, &err);

ocas_vegas_free

Signature:

void ocas_vegas_free(OcasVegas *v);

Description: Frees a Vegas integrator handle. NULL-safe.


ocas_vegas_integrate

Signature:

OcasIntegrateResult ocas_vegas_integrate(OcasVegas *v, ocas_integrand_t f,
                                         void *user_data, int *err);

Description: Integrates f over the unit hypercube (the integrand receives only the first coordinate; see the note above).

Parameters:

ParameterTypeDescription
vOcasVegas*Non-NULL integrator handle
focas_integrand_tIntegrand function
user_datavoid*User data passed to the integrand
errint*May be NULL

Return value: OcasIntegrateResult { double integral; double error; }.

Example:

static double f(double x, void *ud) { return x * x; }

OcasIntegrateResult r = ocas_vegas_integrate(v, f, NULL, &err);
printf("∫₀¹ x² dx ≈ %g ± %g\n", r.integral, r.error);
// Output (sample values; they depend on the seed and sample count): ∫₀¹ x² dx ≈ 0.333 ± 0.001

ocas_vegas_result

Signature:

OcasIntegrateResult ocas_vegas_result(const OcasVegas *v);

Description: Returns the cumulative estimate and error of the most recent ocas_vegas_integrate call.


ocas_vegas_iterations

Signature:

size_t ocas_vegas_iterations(const OcasVegas *v);

Description: Returns the number of completed iterations. NULL returns 0.


ocas_integrate_1d

Signature:

OcasIntegrateResult ocas_integrate_1d(ocas_integrand_t f, void *user_data,
                                      double a, double b,
                                      const OcasVegasOptions *opts, int *err);

Description: Convenience function for one-dimensional numeric integration — integrates over $[a, b]$ using Vegas.

Parameters:

ParameterTypeDescription
focas_integrand_tIntegrand
user_datavoid*User data
adoubleLower integration limit
bdoubleUpper integration limit
optsconst OcasVegasOptions*May be NULL
errint*May be NULL

Return value: OcasIntegrateResult.

Example:

OcasIntegrateResult r = ocas_integrate_1d(f, NULL, 0.0, 1.0, NULL, &err);
printf("result: %g ± %g\n", r.integral, r.error);

8. ODE (3 functions)

Symbolic ordinary differential equation solving. All inputs and outputs are strings.

ocas_ode_classify

Signature:

char *ocas_ode_classify(const char *equation, const char *func,
                        const char *var, int *err);

Description: Classifies an ODE and returns a comma-separated list of applicable solution method names.

Parameters:

ParameterTypeDescription
equationconst char*Expression equal to zero, e.g. "Derivative(y(x), x) - y(x)"
funcconst char*Unknown function name, e.g. "y"
varconst char*Independent variable, e.g. "x"
errint*May be NULL

Return value: A heap-allocated string such as "LinearFirst,PowerSeries"; free with ocas_string_free(). NULL on failure.

Example:

char *methods = ocas_ode_classify("Derivative(y(x),x) - y(x)", "y", "x", NULL);
printf("available methods: %s\n", methods);
// Output: available methods: LinearFirst,PowerSeries
ocas_string_free(methods);

ocas_ode_dsolve

Signature:

char *ocas_ode_dsolve(const char *equation, const char *func,
                      const char *var, const char *hint, int *err);

Description: Solves an ODE symbolically.

Parameters:

ParameterTypeDescription
equationconst char*ODE expression
funcconst char*Unknown function name
varconst char*Independent variable
hintconst char*May be NULL (automatic classification); or one of the method names returned by ocas_ode_classify
errint*May be NULL

Return value: A solution string such as "y = C1*exp(x)" or "unsolved". Free with ocas_string_free().

Example:

char *sol = ocas_ode_dsolve("Derivative(y(x),x) - y(x)", "y", "x", NULL, NULL);
printf("solution: %s\n", sol);  // Output: solution: y = C1*exp(x)
ocas_string_free(sol);

ocas_ode_dsolve_ivp

Signature:

char *ocas_ode_dsolve_ivp(const char *equation, const char *func,
                          const char *var, const char *y0,
                          const char *y1, int *err);

Description: Solves an initial value problem for a first- or second-order linear constant-coefficient ODE via the Laplace transform.

Parameters:

ParameterTypeDescription
equationconst char*ODE expression
funcconst char*Unknown function name
varconst char*Independent variable
y0const char*String expression for $y(0)$, e.g. "1"
y1const char*$y'(0)$ (may be NULL; only needed for second order)
errint*May be NULL

Return value: An explicit solution string without free constants. Free with ocas_string_free().

Example:

// y'' + y = 0, y(0) = 1, y'(0) = 0 → y = cos(x)
char *sol = ocas_ode_dsolve_ivp(
    "Derivative(y(x),x,2) + y(x)", "y", "x", "1", "0", NULL);
printf("IVP solution: %s\n", sol);
ocas_string_free(sol);

9. Number Theory (11 functions)

Arbitrary-precision integers are passed as decimal strings. String results are freed by the caller with ocas_string_free().

ocas_ntheory_factorint

Signature:

char *ocas_ntheory_factorint(const char *n, int *err_out);

Description: Computes the prime factorization of $|n|$.

Parameters:

ParameterTypeDescription
nconst char*Decimal integer string
err_outint*May be NULL

Return value: "p1:e1,p2:e2,..." (ascending); negative numbers start with "-1:1". NULL on failure.

Example:

char *f = ocas_ntheory_factorint("360", NULL);
printf("360 = %s\n", f);  // Output: 360 = 2:3,3:2,5:1
ocas_string_free(f);

f = ocas_ntheory_factorint("-12", NULL);
printf("-12 = %s\n", f);  // Output: -12 = -1:1,2:2,3:1
ocas_string_free(f);

ocas_ntheory_isprime

Signature:

int ocas_ntheory_isprime(const char *n, int *err_out);

Description: BPSW probable primality test.

Return value: 1 = (very likely) prime, 0 = composite, -1 = error.

Example:

int r = ocas_ntheory_isprime("997", NULL);
printf("is 997 prime? %s\n", r == 1 ? "yes" : "no");
// Output: is 997 prime? yes

ocas_ntheory_nextprime

Signature:

char *ocas_ntheory_nextprime(const char *n, int *err_out);

Description: Returns the smallest prime strictly greater than n.

Return value: A decimal string or NULL.

Example:

char *p = ocas_ntheory_nextprime("100", NULL);
printf("next prime after 100: %s\n", p);  // Output: 101
ocas_string_free(p);

ocas_ntheory_discrete_log

Signature:

char *ocas_ntheory_discrete_log(const char *p, const char *base,
                                const char *target, int *err_out);

Description: Solves $\text{base}^x \equiv \text{target} \pmod{p}$. Uses Pohlig–Hellman for prime $p$, BSGS otherwise.

Return value: A decimal string of the logarithm, or NULL (sets OCAS_ERROR_RUNTIME if no solution exists).

Example:

char *x = ocas_ntheory_discrete_log("7", "3", "5", NULL);
// 3^x ≡ 5 (mod 7) → x = 5
printf("discrete log: %s\n", x);
ocas_string_free(x);

ocas_ntheory_crt

Signature:

char *ocas_ntheory_crt(const char *moduli, const char *residues, int *err_out);

Description: Chinese remainder theorem. The moduli need not be coprime; returns NULL if inconsistent.

Parameters:

ParameterTypeDescription
moduliconst char*Comma-separated moduli, e.g. "3,5,7"
residuesconst char*Comma-separated residues, e.g. "2,3,2"

Return value: "r,m" (with $r \equiv \text{residues}[i] \pmod{\text{moduli}[i]}$), or NULL.

Example:

char *r = ocas_ntheory_crt("3,5,7", "2,3,2", NULL);
printf("CRT: %s\n", r);  // Output: CRT: 23,105
// 23 ≡ 2 (mod 3), 23 ≡ 3 (mod 5), 23 ≡ 2 (mod 7)
ocas_string_free(r);

ocas_ntheory_jacobi

Signature:

int ocas_ntheory_jacobi(const char *a, const char *n, int *err_out);

Description: Computes the Jacobi symbol $(a/n)$; n must be a positive odd integer.

Return value: -1, 0, or 1; -2 for invalid input.


ocas_ntheory_totient

Signature:

char *ocas_ntheory_totient(const char *n, int *err_out);

Description: Euler totient function $\varphi(n)$.

Return value: A decimal string or NULL.

Example:

char *t = ocas_ntheory_totient("12", NULL);
printf("φ(12) = %s\n", t);  // Output: φ(12) = 4
ocas_string_free(t);

ocas_ntheory_mobius

Signature:

int ocas_ntheory_mobius(const char *n, int *err_out);

Description: Möbius function $\mu(n)$.

Return value: -1, 0, or 1; -2 for errors.


ocas_ntheory_divisor_count

Signature:

char *ocas_ntheory_divisor_count(const char *n, int *err_out);

Description: Number of positive divisors $\tau(n)$.

Return value: A decimal string or NULL.

Example:

char *d = ocas_ntheory_divisor_count("12", NULL);
printf("τ(12) = %s\n", d);  // Output: τ(12) = 6
// divisors of 12: 1,2,3,4,6,12
ocas_string_free(d);

ocas_ntheory_divisor_sigma

Signature:

char *ocas_ntheory_divisor_sigma(const char *n, uint32_t k, int *err_out);

Description: Sum of the $k$-th powers of the positive divisors, $\sigma_k(n)$.

Parameters:

ParameterTypeDescription
nconst char*Positive integer
kuint32_tPower
err_outint*May be NULL

Return value: A decimal string or NULL.

Example:

char *s = ocas_ntheory_divisor_sigma("6", 1, NULL);
printf("σ₁(6) = %s\n", s);  // Output: σ₁(6) = 12
// 1+2+3+6 = 12
ocas_string_free(s);

ocas_ntheory_liouville

Signature:

int ocas_ntheory_liouville(const char *n, int *err_out);

Description: Liouville function $\lambda(n) = (-1)^{\Omega(n)}$.

Return value: -1, 0, or 1; -2 for errors.


C++ wrapper: ocas::ntheory::*

All number theory functions have C++ inline wrappers that manage string memory automatically:

#include <ocas.h>
#include <ocas.hpp>

std::string f = ocas::ntheory::factorint("360");
// f == "2:3,3:2,5:1"

bool prime = ocas::ntheory::isprime("997");
// prime == true

std::string p = ocas::ntheory::nextprime("100");
// p == "101"

std::string tot = ocas::ntheory::totient("12");
// tot == "4"
C++ functionC functionDescription
factorint(n)ocas_ntheory_factorintPrime factorization
isprime(n)ocas_ntheory_isprimeReturns bool; throws on error
nextprime(n)ocas_ntheory_nextprimeNext prime
discrete_log(p, base, target)ocas_ntheory_discrete_logDiscrete logarithm; throws if no solution
crt(moduli, residues)ocas_ntheory_crtChinese remainder theorem
jacobi(a, n)ocas_ntheory_jacobiJacobi symbol; throws on invalid input
totient(n)ocas_ntheory_totientEuler totient
mobius(n)ocas_ntheory_mobiusMöbius function; throws on invalid input
divisor_count(n)ocas_ntheory_divisor_countNumber of divisors
divisor_sigma(n, k=1)ocas_ntheory_divisor_sigmaSum of divisor powers
liouville(n)ocas_ntheory_liouvilleLiouville function; throws on invalid input

All wrappers throw an ocas::Error exception when the underlying C API reports an error.


10. Tensors (12 functions)

Named-index tensors supporting index contraction and symmetry.

Slot string format

Slots are semicolon-separated; each entry is label,position, where position is "upper" or "lower" (aliases: up/down/contravariant/covariant).

For example: "i,upper;j,lower" denotes two slots $i^j{}_{}$.

ocas_tensor_create

Signature:

OcasTensor *ocas_tensor_create(const char *name, const char *slots,
                               const char *symmetry, int *err);

Description: Creates a tensor.

Parameters:

ParameterTypeDescription
nameconst char*Tensor name, e.g. "T"
slotsconst char*Slot string
symmetryconst char*May be NULL (= "none"); or "none"/"symmetric"/"antisymmetric"
errint*May be NULL

Return value: OcasTensor* handle or NULL.

Example:

OcasTensor *T = ocas_tensor_create("T", "i,upper;j,lower", "symmetric", NULL);
printf("rank: %zu\n", ocas_tensor_rank(T));  // Output: rank: 2
ocas_tensor_free(T);

ocas_tensor_free

Signature:

void ocas_tensor_free(OcasTensor *t);

Description: Frees a tensor handle. NULL-safe.


ocas_tensor_name

Signature:

char *ocas_tensor_name(const OcasTensor *t, int *err);

Description: Returns a heap-allocated string of the tensor name. Free with ocas_string_free().


ocas_tensor_rank

Signature:

size_t ocas_tensor_rank(const OcasTensor *t);

Description: Returns the rank of the tensor (number of slots). NULL returns 0.


ocas_tensor_symmetry

Signature:

int ocas_tensor_symmetry(const OcasTensor *t);

Description: Returns the symmetry code.

Return value: 0 = none, 1 = symmetric, 2 = antisymmetric, -1 = null handle.


ocas_tensor_to_string

Signature:

char *ocas_tensor_to_string(const OcasTensor *t, int *err);

Description: Returns the string representation name(slot, slot, ...) of the tensor. Free with ocas_string_free().


ocas_tensor_symmetrise_sign

Signature:

int64_t ocas_tensor_symmetrise_sign(const OcasTensor *t);

Description: Returns the antisymmetrization sign ($+1$ or $-1$). NULL returns 0.


ocas_tensor_contract

Signature:

int ocas_tensor_contract(const OcasTensor *a, const OcasTensor *b,
                         OcasTensorContraction *out, int *err);

Description: Contracts over the shared dummy indices (identical labels with opposite variance) of two tensors.

Parameters:

ParameterTypeDescription
aconst OcasTensor*Non-NULL tensor
bconst OcasTensor*Non-NULL tensor
outOcasTensorContraction*Output struct
errint*May be NULL

Return value: OCAS_OK (0) on success.

OcasTensorContraction fields:

FieldTypeDescription
kindint0 = product (free indices remain), 1 = scalar (fully contracted)
tensorsOcasTensor**Valid when kind == 0; array of tensor handles
n_tensorssize_tLength of the tensors array
scalar_strchar*Valid when kind == 1; string of the scalar result

Memory:

  • kind == 0: free each tensors[i] with ocas_tensor_free(), and the array with ocas_tensor_contraction_free().
  • kind == 1: free scalar_str with ocas_string_free(), and the struct with ocas_tensor_contraction_free().

Example:

OcasTensor *A = ocas_tensor_create("A", "i,upper;j,lower", NULL, NULL);
OcasTensor *B = ocas_tensor_create("B", "j,upper;k,lower", NULL, NULL);
OcasTensorContraction result = {0};
if (ocas_tensor_contract(A, B, &result, NULL) == OCAS_OK) {
    if (result.kind == 0) {
        // free indices remain
        for (size_t i = 0; i < result.n_tensors; i++) {
            char *s = ocas_tensor_to_string(result.tensors[i], NULL);
            printf("result tensor: %s\n", s);
            ocas_string_free(s);
            ocas_tensor_free(result.tensors[i]);
        }
    }
    ocas_tensor_contraction_free(&result);
}
ocas_tensor_free(B);
ocas_tensor_free(A);

ocas_tensor_contraction_free

Signature:

void ocas_tensor_contraction_free(OcasTensorContraction *c);

Description: Frees the tensors array and scalar_str of a contraction result. Does not free the individual tensor handles. NULL-safe.


ocas_tensor_canonicalize

Signature:

char *ocas_tensor_canonicalize(const char *expr_str, const char *specs_str,
                                const char *groups_str, int *err);

Description: Canonicalizes a tensor expression via graph isomorphism (0.22.0).

Parameters:

ParameterTypeDescription
expr_strconst char*Tensor product string, e.g. "T(i,j)*U(j,k)"
specs_strconst char*Comma-separated name:sym pairs, e.g. "T:none,U:none"
groups_strconst char*May be NULL; or comma-separated label:group pairs
errint*May be NULL

Return value: A heap-allocated canonical form string. Free with ocas_string_free().

Example:

char *canon = ocas_tensor_canonicalize(
    "T(i,j)*T(j,i)", "T:symmetric", NULL, NULL);
printf("canonical form: %s\n", canon);
ocas_string_free(canon);

ocas_young_project

Signature:

char *ocas_young_project(const char *expr_str, const char *tableau_str, int *err);

Description: Applies a Young projector to a tensor expression (0.22.0).

Parameters:

ParameterTypeDescription
expr_strconst char*Tensor expression, e.g. "f(a,b)"
tableau_strconst char*Comma-separated row lengths, e.g. "2,1" denotes □□/□
errint*May be NULL

Return value: A heap-allocated expanded expression string. Free with ocas_string_free().

Example:

// fully antisymmetric projection ((1,1,1) Young tableau, 3 indices)
char *proj = ocas_young_project("f(a,b,c)", "1,1,1", NULL);
printf("projection result: %s\n", proj);
ocas_string_free(proj);

ocas_tensor_refresh_dummies

Signature:

char *ocas_tensor_refresh_dummies(const char *expr_str, const char *specs_str, int *err);

Description: Renames the dummy indices (labels occurring exactly twice) of a tensor expression to d0, d1, ... to avoid conflicts (0.22.0).

Parameters:

ParameterTypeDescription
expr_strconst char*Tensor expression
specs_strconst char*name:sym pairs
errint*May be NULL

Return value: A heap-allocated string. Free with ocas_string_free().


C++ wrapper: ocas::tensor::*

std::string canon = ocas::tensor::canonicalize("T(i,j)*T(j,i)", "T:symmetric");
std::string proj = ocas::tensor::young_project("f(a,b,c)", "2,1");
std::string fresh = ocas::tensor::refresh_dummies("T(i,j)*U(j,i)", "T:none,U:none");

All wrappers throw ocas::Error on error.


11. Dual Numbers (14 functions)

Hyper-dual numbers (forward automatic differentiation), supporting only Rational coefficients. Polynomial/rational arithmetic.

Coefficient string format

Rationals are passed as "num" (denominator 1) or "num/den". Returned strings use the same format.

ocas_dual_shape_new

Signature:

OcasDualShape *ocas_dual_shape_new(size_t n_vars, int *err);

Description: Creates a first-order shape tracking derivatives with respect to n_vars variables.

Return value: OcasDualShape* handle or NULL.

Example:

OcasDualShape *shape = ocas_dual_shape_new(2, NULL);  // 2 variables
printf("n vars: %zu, n components: %zu\n",
       ocas_dual_shape_n_vars(shape),
       ocas_dual_shape_n_components(shape));
// Output: n vars: 2, n components: 3
ocas_dual_shape_free(shape);

ocas_dual_shape_free

Signature:

void ocas_dual_shape_free(OcasDualShape *s);

Description: Frees a shape handle. NULL-safe.


ocas_dual_shape_n_vars

Signature:

size_t ocas_dual_shape_n_vars(const OcasDualShape *s);

Description: Returns the number of differentiation variables. NULL returns 0.


ocas_dual_shape_n_components

Signature:

size_t ocas_dual_shape_n_components(const OcasDualShape *s);

Description: Returns the total number of components (value + each partial derivative). NULL returns 0.


ocas_dual_variable

Signature:

OcasHyperDual *ocas_dual_variable(const OcasDualShape *shape, size_t i,
                                  const char *coeff, int *err);

Description: Creates an independent variable $x_i = \text{coeff}$ (derivative with respect to variable $i$ is 1).

Parameters:

ParameterTypeDescription
shapeconst OcasDualShape*Non-NULL shape handle
isize_tVariable index ($0 \le i < \text{n_vars}$)
coeffconst char*Coefficient string "num" or "num/den"
errint*May be NULL

Return value: OcasHyperDual* handle or NULL.


ocas_dual_constant

Signature:

OcasHyperDual *ocas_dual_constant(const OcasDualShape *shape,
                                  const char *coeff, int *err);

Description: Creates a constant dual number (all derivatives zero).


ocas_hyperdual_free

Signature:

void ocas_hyperdual_free(OcasHyperDual *d);

Description: Frees a hyper-dual number handle. NULL-safe.


ocas_dual_value

Signature:

char *ocas_dual_value(const OcasHyperDual *d, int *err);

Description: Returns a heap-allocated string of the scalar value component. Free with ocas_string_free().


ocas_dual_deriv

Signature:

char *ocas_dual_deriv(const OcasHyperDual *d, size_t i, int *err);

Description: Returns a heap-allocated string of the partial derivative with respect to variable $i$. Returns NULL if the shape has no first-order component for $i$.

Example:

OcasDualShape *shape = ocas_dual_shape_new(2, NULL);
OcasHyperDual *x = ocas_dual_variable(shape, 0, "3", NULL);   // x₀ = 3
OcasHyperDual *y = ocas_dual_variable(shape, 1, "5", NULL);   // x₁ = 5
OcasHyperDual *prod = ocas_dual_mul(x, y, NULL);               // f = x₀·x₁

char *val = ocas_dual_value(prod, NULL);
char *df_dx = ocas_dual_deriv(prod, 0, NULL);  // ∂f/∂x₀ = x₁ = 5
char *df_dy = ocas_dual_deriv(prod, 1, NULL);  // ∂f/∂x₁ = x₀ = 3

printf("f = %s, ∂f/∂x₀ = %s, ∂f/∂x₁ = %s\n", val, df_dx, df_dy);
// Output: f = 15, ∂f/∂x₀ = 5, ∂f/∂x₁ = 3

ocas_string_free(val);
ocas_string_free(df_dx);
ocas_string_free(df_dy);
ocas_hyperdual_free(prod);
ocas_hyperdual_free(y);
ocas_hyperdual_free(x);
ocas_dual_shape_free(shape);

ocas_dual_add

Signature:

OcasHyperDual *ocas_dual_add(const OcasHyperDual *a, const OcasHyperDual *b, int *err);

Description: Computes $a + b$. Both operands must share the same shape.

Return value: A new handle or NULL.


ocas_dual_sub

Signature:

OcasHyperDual *ocas_dual_sub(const OcasHyperDual *a, const OcasHyperDual *b, int *err);

Description: Computes $a - b$.


ocas_dual_mul

Signature:

OcasHyperDual *ocas_dual_mul(const OcasHyperDual *a, const OcasHyperDual *b, int *err);

Description: Computes $a \times b$. Derivatives are propagated automatically via the product rule.


ocas_dual_div

Signature:

OcasHyperDual *ocas_dual_div(const OcasHyperDual *a, const OcasHyperDual *b, int *err);

Description: Computes $a / b$. If the value component of $b$ is zero, sets OCAS_ERROR_DIVISION_BY_ZERO and returns NULL.


ocas_dual_neg

Signature:

OcasHyperDual *ocas_dual_neg(const OcasHyperDual *a, int *err);

Description: Computes $-a$.


12. Gröbner (8 functions)

Gröbner basis computation and polynomial system solving.

⚠️ Non-standard conventions

  • These functions are not included in the ocas.h header; declare the prototypes yourself.
  • Error codes use -1 instead of the OCAS_* constants.
  • The OcasGroebnerBasis and OcasSystemSolution handles are not marked #[repr(C)].

Polynomial data format

Polynomials are passed as arrays of coefficients; each polynomial is specified by:

  • n_vars — number of variables
  • n_terms — number of terms
  • exponents — flattened exponent matrix (n_terms × n_vars, row-major)
  • coeff_nums / coeff_dens — numerator/denominator arrays of the rational coefficients

ocas_groebner_basis

Signature:

OcasGroebnerBasis *ocas_groebner_basis(
    size_t n_polys,
    const size_t *n_vars_array,
    const size_t *n_terms_array,
    const size_t *exponents,
    const int64_t *coeff_nums,
    const int64_t *coeff_dens,
    int32_t algorithm,
    int32_t *err
);

Description: Computes the Gröbner basis from an array of polynomial data.

Parameters:

ParameterTypeDescription
n_polyssize_tNumber of polynomials
n_vars_arrayconst size_t*Number of variables for each polynomial
n_terms_arrayconst size_t*Number of terms for each polynomial
exponentsconst size_t*Flattened exponent matrix
coeff_numsconst int64_t*Numerators of the rational coefficients
coeff_densconst int64_t*Denominators of the rational coefficients
algorithmint32_tAlgorithm selection: 0 = Auto, 1 = F4, 2 = F5, 3 = Buchberger
errint32_t*Error code (-1 means failure). Note: unlike the rest of the C API, this pointer must NOT be NULL — the function returns NULL without computing when it is

Return value: OcasGroebnerBasis* handle or NULL.

Memory: The caller frees with ocas_groebner_basis_free().

Example:

// Compute the Gröbner basis of {x² + y - 1, x + y² - 1}
size_t n_vars[] = {2, 2};
size_t n_terms[] = {2, 2};
// x² + y - 1: exponents = [[2,0],[0,1],[0,0]]
// x + y² - 1: exponents = [[1,0],[0,2],[0,0]]
size_t exps[] = {2,0, 0,1, 0,0,
                 1,0, 0,2, 0,0};
int64_t nums[] = {1, 1, -1,  1, 1, -1};
int64_t dens[] = {1, 1,  1,  1, 1,  1};

int32_t err = 0;
OcasGroebnerBasis *gb = ocas_groebner_basis(
    2, n_vars, n_terms, exps, nums, dens, 0, &err);
if (gb) {
    printf("the basis has %zu elements\n", ocas_groebner_basis_len(gb));
    printf("zero-dimensional? %s\n", ocas_is_zero_dimensional(gb) ? "yes" : "no");
    ocas_groebner_basis_free(gb);
}

ocas_groebner_basis_free

Signature:

void ocas_groebner_basis_free(OcasGroebnerBasis *gb);

Description: Frees a Gröbner basis handle. NULL-safe.


ocas_groebner_basis_len

Signature:

size_t ocas_groebner_basis_len(const OcasGroebnerBasis *gb);

Description: Returns the number of elements in the Gröbner basis. NULL returns 0.


ocas_is_zero_dimensional

Signature:

bool ocas_is_zero_dimensional(const OcasGroebnerBasis *gb);

Description: Checks whether the ideal is zero-dimensional. NULL returns false.


ocas_solve_polynomial_system

Signature:

OcasSystemSolution *ocas_solve_polynomial_system(
    size_t n_polys,
    const size_t *n_vars_array,
    const size_t *n_terms_array,
    const size_t *exponents,
    const int64_t *coeff_nums,
    const int64_t *coeff_dens,
    int32_t algorithm,
    int32_t *err
);

Description: Solves a system of polynomial equations. Returns a handle or NULL.

Parameters: Same as ocas_groebner_basis.

Memory: The caller frees with ocas_system_solution_free().


ocas_system_solution_count

Signature:

size_t ocas_system_solution_count(const OcasSystemSolution *sol);

Description: Returns the number of solutions. Returns 0 for positive-dimensional or empty solution sets. NULL returns 0.


ocas_system_solution_value

Signature:

double ocas_system_solution_value(const OcasSystemSolution *sol,
                                  size_t sol_idx, size_t var_idx);

Description: Gets the value of a specific variable of a specific solution.

Parameters:

ParameterTypeDescription
solconst OcasSystemSolution*Non-NULL solution handle
sol_idxsize_tIndex of the solution
var_idxsize_tIndex of the variable

Return value: An f64 value. Returns 0.0 on out-of-range or error.

Example:

OcasSystemSolution *sol = ocas_solve_polynomial_system(
    2, n_vars, n_terms, exps, nums, dens, 0, &err);
if (sol) {
    size_t count = ocas_system_solution_count(sol);
    printf("found %zu solutions\n", count);
    for (size_t i = 0; i < count; i++) {
        printf("solution %zu: x = %g, y = %g\n", i,
               ocas_system_solution_value(sol, i, 0),
               ocas_system_solution_value(sol, i, 1));
    }
    ocas_system_solution_free(sol);
}

ocas_system_solution_free

Signature:

void ocas_system_solution_free(OcasSystemSolution *sol);

Description: Frees a solution handle. NULL-safe.


Complete Examples

C: Symbolic differentiation and evaluation

#include <ocas.h>
#include <stdio.h>
#include <assert.h>

int main(void) {
    int err = 0;

    // Parse the expression
    OcasExpr *f = ocas_expr_parse("x^3 - 6*x^2 + 11*x - 6", &err);
    assert(f != NULL);

    // Differentiate
    OcasExpr *df = ocas_expr_diff(f, "x", &err);
    assert(df != NULL);

    // Simplify
    OcasExpr *simplified = ocas_expr_simplify(df, &err);
    assert(simplified != NULL);

    char *s = ocas_expr_to_string(simplified, &err);
    printf("f'(x) = %s\n", s);  // Output: f'(x) = 11 + (-12*x) + (3*(x^2))

    // Cleanup
    ocas_string_free(s);
    ocas_expr_free(simplified);
    ocas_expr_free(df);
    ocas_expr_free(f);

    return 0;
}

Compile:

gcc example.c -locas -o example

C: Automatic differentiation (dual numbers)

#include <ocas.h>
#include <stdio.h>

int main(void) {
    int err = 0;

    // f(x,y) = x² + 2xy + y², evaluated at (3, 5) with its partial derivatives
    OcasDualShape *shape = ocas_dual_shape_new(2, &err);
    OcasHyperDual *x = ocas_dual_variable(shape, 0, "3", &err);   // x₀ = 3
    OcasHyperDual *y = ocas_dual_variable(shape, 1, "5", &err);   // x₁ = 5

    OcasHyperDual *x2 = ocas_dual_mul(x, x, &err);       // x², ∂/∂x = 6
    OcasHyperDual *xy = ocas_dual_mul(x, y, &err);       // xy,  ∂/∂x = 5
    OcasHyperDual *y2 = ocas_dual_mul(y, y, &err);       // y²,  ∂/∂y = 10
    OcasHyperDual *two_xy = ocas_dual_add(xy, xy, &err);
    OcasHyperDual *a = ocas_dual_add(x2, two_xy, &err);
    OcasHyperDual *f = ocas_dual_add(a, y2, &err);

    char *v = ocas_dual_value(f, &err);
    char *dfx = ocas_dual_deriv(f, 0, &err);
    char *dfy = ocas_dual_deriv(f, 1, &err);
    printf("f(3,5) = %s, ∂f/∂x = %s, ∂f/∂y = %s\n", v, dfx, dfy);

    ocas_string_free(v);
    ocas_string_free(dfx);
    ocas_string_free(dfy);
    ocas_hyperdual_free(f);
    ocas_hyperdual_free(a);
    ocas_hyperdual_free(two_xy);
    ocas_hyperdual_free(y2);
    ocas_hyperdual_free(xy);
    ocas_hyperdual_free(x2);
    ocas_hyperdual_free(y);
    ocas_hyperdual_free(x);
    ocas_dual_shape_free(shape);
    return 0;
}

Note: the dual-number domain has no C++ RAII wrappers in ocas.hpp; use the C API above directly.

C: Number theory

#include <ocas.h>
#include <stdio.h>

int main(void) {
    int err = 0;

    // Prime factorization
    char *f = ocas_ntheory_factorint("360", &err);
    printf("360 = %s\n", f);  // 2:3,3:2,5:1
    ocas_string_free(f);

    // Primality test
    printf("is 997 prime? %s\n", ocas_ntheory_isprime("997", &err) ? "yes" : "no");

    // Chinese remainder theorem
    char *r = ocas_ntheory_crt("3,5,7", "2,3,2", &err);
    printf("CRT: %s\n", r);  // 23,105
    ocas_string_free(r);

    // Euler totient
    char *t = ocas_ntheory_totient("100", &err);
    printf("φ(100) = %s\n", t);  // 40
    ocas_string_free(t);

    return 0;
}

See Also

Mathematics Overview

This chapter systematically introduces the mathematical theory involved in oCAS, from basic concepts to advanced algorithms, providing the complete mathematical background needed to understand the source implementation. Every article follows a progressive structure: "Prerequisites → Basic Concepts → Core Theory → oCAS Implementation → Advanced Topics → References".


Knowledge Map

The diagram below shows the dependencies between the mathematical topics. An arrow indicates that the source topic is recommended to be studied before the target topic.

graph TD
    PA[Polynomial Algebra] --> FA[Finite Fields &<br>Modular Arithmetic]
    LA[Linear Algebra] --> FA
    PA --> GCD[Polynomial GCD<br>& Factorization]
    FA --> GCD
    LA --> GCD
    FA --> NT[Number Theory Algorithms]
    PA --> GB[Gröbner Basis<br>Theory]
    LA --> GB
    GCD --> GB
    GCD --> ANF[Algebraic Number Fields &<br>Galois Theory]
    GB --> ANF
    GB --> FGLM[FGLM &<br>Elimination Theory]
    NT --> ANF
    FA --> ANF
    SC[Symbolic Calculus] --> ODE[ODE Solving<br>Theory]
    PA --> ODE
    LA --> ODE
    SC --> RISCH[Risch Integration<br>Algorithm]
    ODE --> RISCH
    PA --> RISCH
    FA --> RISCH

    TENSOR[Tensor Algebra &<br>Canonicalization]
    MC[Monte Carlo<br>Integration]

    style PA fill:#e8f5e9,stroke:#2e7d32
    style FA fill:#e3f2fd,stroke:#1565c0
    style LA fill:#fff3e0,stroke:#e65100
    style SC fill:#fce4ec,stroke:#c62828
    style GCD fill:#e8f5e9,stroke:#2e7d32
    style NT fill:#e3f2fd,stroke:#1565c0
    style GB fill:#f3e5f5,stroke:#6a1b9a
    style ODE fill:#fce4ec,stroke:#c62828
    style RISCH fill:#fce4ec,stroke:#c62828
    style FGLM fill:#f3e5f5,stroke:#6a1b9a
    style ANF fill:#e3f2fd,stroke:#1565c0
    style TENSOR fill:#fffde7,stroke:#f57f17
    style MC fill:#fffde7,stroke:#f57f17

Depending on your interests, choose one of the following paths for systematic study.

Path A: Polynomial Algebra & Symbolic Computation

For readers who want to understand the core algorithms of symbolic computation (polynomial arithmetic, elimination, solving systems of equations).

$$\text{Polynomial Algebra} ;\to; \text{GCD/Factorization} ;\to; \text{Gröbner Bases} ;\to; \text{FGLM/Elimination}$$

StepTopicKey TakeawaysFile
1Polynomial AlgebraPolynomial rings, monomial orders, the division algorithmpolynomial-algebra.md
2Linear AlgebraMatrix operations, the Bareiss algorithm, Gaussian eliminationlinear-algebra.md
3Finite Fields & Modular ArithmeticConstruction of $\mathbb{F}_p$, modular inverses, NTTfinite-fields.md
4Polynomial GCD & FactorizationEuclid's algorithm, Hensel lifting, Berlekamp's algorithmpoly-gcd-factoring.md
5Gröbner Basis TheoryBuchberger's algorithm, F4/F5, Hilbert functionsgroebner-theory.md
6FGLM & Elimination TheoryOrder-change algorithms, ideal operations, primary decompositionfglm-elimination.md

Path B: Number Theory & Algebraic Number Fields

For readers who want to understand integer factorization, primality testing, discrete logarithms, and the construction of algebraic number fields.

$$\text{Finite Fields} ;\to; \text{Number Theory Algorithms} ;\to; \text{Algebraic Number Fields}$$

StepTopicKey TakeawaysFile
1Finite Fields & Modular ArithmeticConstruction of $\mathbb{F}p$ and $\mathbb{F}{p^d}$, cyclicity of the multiplicative groupfinite-fields.md
2Number Theory AlgorithmsBPSW primality, ECM factorization, BSGS discrete logarithms, Tonelli–Shanksnumber-theory-algorithms.md
3Algebraic Number Fields & Galois TheoryRepresentation of $\mathbb{Q}(\alpha)$, Trager's norm algorithm, $\mathrm{GF}(p^d)$algebraic-number-fields.md

Path C: Symbolic Calculus & Differential Equations

For readers who want to understand symbolic differentiation, integration, ODE solving, and the Risch algorithm.

$$\text{Symbolic Calculus} ;\to; \text{ODE Solving} ;\to; \text{Risch Integration}$$

StepTopicKey TakeawaysFile
1Symbolic CalculusDifferentiation rules, Taylor expansion, expression-tree transformationssymbolic-calculus.md
2ODE Solving TheorySeparable/linear/Bernoulli/constant-coefficient/series solutions/Laplace transformsode-theory.md
3The Risch Integration AlgorithmDifferential-field towers, Hermite reduction, the logarithmic derivative identity, RDErisch-algorithm.md

Topic Quick-Reference Table

The table below lists the oCAS source module and the chapter file in this document for each mathematical topic.

TopicLeveloCAS ModuleMath Chapter
Polynomial AlgebraFoundationsocas-poly (dense.rs, sparse.rs)polynomial-algebra.md
Finite Fields & Modular ArithmeticFoundationsocas-domain (finite_field.rs)finite-fields.md
Linear AlgebraFoundationsocas-poly (matrix.rs)linear-algebra.md
Polynomial GCD & FactorizationAdvancedocas-poly (gcd/, factor/)poly-gcd-factoring.md
Gröbner Basis TheoryAdvancedocas-poly (groebner/mod.rs, f4.rs, f5.rs, hilbert.rs)groebner-theory.md
Number Theory AlgorithmsAdvancedocas-domain (number_theory/)number-theory-algorithms.md
Symbolic CalculusAdvancedocas-calc (lib.rs)symbolic-calculus.md
ODE Solving TheoryAdvancedocas-calc (ode/)ode-theory.md
The Risch Integration AlgorithmHigherocas-calc (integral/)risch-algorithm.md
FGLM & Elimination TheoryHigherocas-poly (groebner/fglm.rs, ideal.rs)fglm-elimination.md
Algebraic Number Fields & Galois TheoryHigherocas-domain (algebraic.rs)algebraic-number-fields.md
Tensor Algebra & CanonicalizationHigherocas-atom (tensor/canon.rs, young.rs, spec.rs)tensor-canonicalization.md
Monte Carlo IntegrationHigherocas-eval (numeric/vegas.rs)monte-carlo-integration.md

Correspondence Between Mathematical Branches and oCAS

The design of oCAS follows the tradition of computer algebra. The correspondence between modules and mathematical branches is as follows:

  • Commutative algebra (polynomial rings, ideal theory) → ocas-poly: polynomial representation, GCD, factorization, Gröbner bases, ideal operations
  • Number theory (primality, factorization, discrete logarithms) → ocas-domain/number_theory: integer factorization, primality testing, quadratic residues
  • Linear algebra (matrices, determinants) → ocas-poly/matrix: the Bareiss determinant, Gaussian elimination, linear systems
  • Calculus and differential algebra (symbolic differentiation, integration, ODEs) → ocas-calc: differentiation, Taylor expansion, ODE classification and solving, the Risch algorithm
  • Tensor algebra (index contraction, symmetrization) → ocas-atom/tensor: graph encoding, the McKay algorithm, Young projection
  • Numerical analysis (adaptive integration) → ocas-eval/numeric: Vegas Monte Carlo, adaptive grids

Foundations: Polynomial Algebra

Polynomial algebra is the cornerstone of symbolic computation. This chapter starts from the basic definitions of polynomial rings, progressively introduces monomial orders, the multivariate division algorithm, and Hilbert's basis theorem, and explains how oCAS implements these concepts in Rust.

Prerequisites

Before reading this chapter, the following background is recommended:

  • Abstract algebra: basic notions of groups, rings, and fields (identity element, zero divisors, integral domains)
  • Linear algebra: vector spaces, linear independence, bases
  • Mathematical induction: used to prove termination of division algorithms

For review, see the learning-path recommendations in the Mathematics Overview.

Basic Concepts

Univariate Polynomial Rings

Let $\mathbb{F}$ be a field (such as $\mathbb{Q}$ or $\mathbb{F}_p$). The univariate polynomial ring $\mathbb{F}[x]$ is the set of all expressions of the form

$$f(x) = a_0 + a_1 x + a_2 x^2 + \cdots + a_n x^n$$

where $a_i \in \mathbb{F}$ and $n \geq 0$.

Definition (degree). For a nonzero polynomial $f = \sum_{i=0}^n a_i x^i$ ($a_n \neq 0$), its degree is

$$\deg(f) = n$$

The degree of the zero polynomial $f = 0$ is conventionally $-\infty$ (represented as None in implementations).

Definition (leading coefficient). The leading coefficient of $f$ is $\text{lc}(f) = a_n$. When $\text{lc}(f) = 1$, $f$ is called monic.

Properties:

OperationDegree relation
$f + g$$\deg(f+g) \leq \max(\deg f, \deg g)$
$f \cdot g$$\deg(f \cdot g) = \deg f + \deg g$
$f / g$ (division with remainder)$\deg(r) < \deg(g)$

Multivariate Polynomial Rings

Let $\mathbb{F}$ be a field and $x_1, x_2, \ldots, x_n$ be $n$ indeterminates. The multivariate polynomial ring $\mathbb{F}[x_1, \ldots, x_n]$ is the set of all finite sums

$$f = \sum_{\alpha \in \mathbb{N}^n} c_\alpha , \mathbf{x}^\alpha$$

where $\alpha = (\alpha_1, \ldots, \alpha_n) \in \mathbb{N}^n$ is called an exponent vector, $\mathbf{x}^\alpha = x_1^{\alpha_1} x_2^{\alpha_2} \cdots x_n^{\alpha_n}$ is called a monomial, and $c_\alpha \in \mathbb{F}$ is called a coefficient.

Definition (total degree). The total degree of a monomial $\mathbf{x}^\alpha$ is $|\alpha| = \alpha_1 + \cdots + \alpha_n$. The total degree of a polynomial $f$ is $\deg(f) = \max{|\alpha| : c_\alpha \neq 0}$.

Definition (partial degree). The partial degree of a polynomial $f$ with respect to the variable $x_i$ is $\deg_{x_i}(f) = \max{\alpha_i : c_\alpha \neq 0}$.

Important distinction: The univariate polynomial ring $\mathbb{F}[x]$ is a principal ideal domain (PID) — every ideal is generated by a single polynomial. The multivariate polynomial ring $\mathbb{F}[x_1, \ldots, x_n]$ ($n \geq 2$) is no longer a PID; this is the starting point of Gröbner basis theory — see Gröbner Basis Theory.

Internal Representations of Polynomials

There are two basic computer representations of polynomials:

RepresentationStorageUse cases
Dense representationCoefficient vector $[a_0, a_1, \ldots, a_n]$Univariate, moderate degrees
Sparse representationStores only the nonzero-term pairs ${(\alpha, c_\alpha)}$Multivariate, many zero terms

In oCAS, DenseUnivariatePolynomial uses the dense representation, while SparseMultivariatePolynomial uses the sparse representation. See § Implementation in oCAS.

Monomial Orders

In a multivariate polynomial ring, "who is the leading term" is no longer obvious — we need a rule for comparing monomials.

Definition (monomial order). A monomial order on $\mathbb{N}^n$ is a total order $\succ$ satisfying:

  1. Well-ordering: every nonempty subset of $\mathbb{N}^n$ has a least element (equivalently: $0 \preceq \alpha$ for all $\alpha$)
  2. Compatibility: if $\alpha \succ \beta$, then $\alpha + \gamma \succ \beta + \gamma$ for all $\gamma \in \mathbb{N}^n$

These two conditions guarantee termination of polynomial division. The three most commonly used monomial orders are introduced below.

Lexicographic Order (Lex)

$x^\alpha \succ_{\text{lex}} x^\beta$ if and only if the leftmost nonzero component of $\alpha - \beta$ is positive.

Equivalent description: compare the exponent vectors component by component from left to right; the first position where they differ decides the order.

Example: In $\mathbb{F}[x, y, z]$, ordered lexicographically:

$$x^2 y^3 z \succ_{\text{lex}} x^2 y^2 z^3 \succ_{\text{lex}} x y^5 \succ_{\text{lex}} y^{100} \succ_{\text{lex}} z^{100}$$

The characteristic of the lexicographic order is "elimination": if $x_1 \succ x_2 \succ \cdots \succ x_n$, then the leading term under the lexicographic order has maximal degree in $x_1$. This makes lexicographic bases naturally elimination-oriented.

Graded Lexicographic Order (Grlex)

$x^\alpha \succ_{\text{grlex}} x^\beta$ if and only if:

  1. $|\alpha| > |\beta|$, or
  2. $|\alpha| = |\beta|$ and $\alpha \succ_{\text{lex}} \beta$

That is, sort by total degree first; within the same total degree, sort lexicographically.

Example: In $\mathbb{F}[x, y]$, $\deg(x^2) = 2$, $\deg(xy) = 2$, $\deg(y^3) = 3$, hence

$$y^3 \succ_{\text{grlex}} x^2 \succ_{\text{grlex}} xy$$

Graded Reverse Lexicographic Order (Grevlex)

$x^\alpha \succ_{\text{grevlex}} x^\beta$ if and only if:

  1. $|\alpha| > |\beta|$, or
  2. $|\alpha| = |\beta|$ and the rightmost nonzero component of $\alpha - \beta$ is negative

That is, sort by total degree first; within the same total degree, compare from right to left, and the smaller one is greater.

Example: In $\mathbb{F}[x, y, z]$, $x^2 y$ and $x y^2$ both have total degree 3. $\alpha - \beta = (1, -1, 0)$; the rightmost nonzero component is $-1$ (negative), hence

$$x^2 y \succ_{\text{grevlex}} x y^2$$

The key advantage of Grevlex is computational: among monomials of the same total degree, Grevlex tends to keep the exponents of low-indexed variables smaller, which produces smaller intermediate polynomials in Gröbner basis computations.

Comparing the Three Orders

MonomialLexGrlexGrevlex
$x^2$ ($\alpha = (2,0)$)122
$xy$ ($\alpha = (1,1)$)233
$y^3$ ($\alpha = (0,3)$)31 (highest degree)1 (highest degree)

Note: In the table above, rank 1 means "largest" under the corresponding order. Both Grlex and Grevlex sort by total degree first, so $y^3$ (total degree 3) comes before $x^2$ and $xy$ (total degree 2). In two variables, Grlex and Grevlex coincide: for $x^2$ and $xy$, the rightmost nonzero component of $(2,0) - (1,1) = (1, -1)$ is $-1 < 0$, so both orders give $x^2 \succ xy$. The two orders differ only from $n \geq 3$ variables onward; for example, compare $xz^2$ and $y^3$ (total degree 3 each): under Grlex, $xz^2 \succ y^3$ (lexicographic tie-break: $(1,0,2) > (0,3,0)$), while under Grevlex, $(0,3,0) - (1,0,2) = (-1,3,-2)$ has rightmost nonzero component $-2 < 0$, so $y^3 \succ_{\text{grevlex}} xz^2$.

Re-verify the Grevlex tie-break: $x^2 y$ vs $x y^2$: $(2,1) - (1,2) = (1, -1)$; the rightmost nonzero component is $-1 < 0$, so $x^2 y \succ_{\text{grevlex}} xy^2$.

Extended Orders

Besides the three basic orders above, oCAS also supports the following parameterized orders:

Weight order (WeightOrder): assigns a weight $w_i$ to each variable and orders by the decreasing value of $\sum w_i \alpha_i$. When the weight vector is $(1, 1, \ldots, 1)$, this degenerates to the total-degree order.

Matrix order (MatrixOrder): given an $n \times n$ integer matrix $M$, $\alpha \succ \beta$ if and only if $M\alpha >_{\text{lex}} M\beta$. The matrix order is the complete generalization of all linear monomial orders. In particular, an elimination order can be constructed via MatrixOrder::elimination_order(elim_vars, n_vars), which is used to eliminate variables from polynomial systems.

Block order (BlockOrder): partitions the variables into contiguous blocks, each block using an independent sub-order (Lex, Grlex, or Grevlex). The standard form of an elimination order is "the first $k$ variables use Lex, the rest use Grevlex".

Core Theory

Univariate Division with Remainder

Theorem (division with remainder). Let $\mathbb{F}$ be a field and $f, g \in \mathbb{F}[x]$ with $g \neq 0$. There exist unique $q, r \in \mathbb{F}[x]$ such that

$$f = q \cdot g + r, \quad \deg(r) < \deg(g)$$

Algorithm (successive elimination of leading terms):

Input: f, g (g ≠ 0)
Output: quotient q, remainder r

q ← 0, r ← f
while r ≠ 0 and deg(r) ≥ deg(g):
    t ← lc(r) / lc(g) · x^(deg(r) - deg(g))
    q ← q + t
    r ← r - t · g
return (q, r)

Termination: at each step $\deg(r)$ strictly decreases (or $r$ becomes zero), so the algorithm always terminates.

Generalization to integral domains: When the coefficient ring is a Euclidean domain (such as $\mathbb{Z}$), division with remainder still exists, but pseudo-division is needed to avoid fractions. In oCAS, DenseUnivariatePolynomial::div_rem implements this by performing division with remainder on the leading coefficients via EuclideanDomain::div_rem.

Multivariate Division with Remainder

The multivariate case is far more complicated than the univariate one — the quotient and remainder are not unique; they depend on the monomial order and the order of division.

Theorem (multivariate division algorithm). Let $\mathbb{F}$ be a field and $f, g_1, \ldots, g_s \in \mathbb{F}[x_1, \ldots, x_n]$ with $g_i \neq 0$. Fix a monomial order. There is an algorithm that computes $q_1, \ldots, q_s, r \in \mathbb{F}[x_1, \ldots, x_n]$ such that

$$f = q_1 g_1 + q_2 g_2 + \cdots + q_s g_s + r$$

where no nonzero term of $r$ is divisible by any $\text{lt}(g_i)$. Such an $r$ is called the remainder (or normal form) of $f$ with respect to ${g_1, \ldots, g_s}$.

Algorithm:

Input: f, G = [g1, ..., gs]
Output: q1, ..., qs, r

r ← 0, p ← f
while p ≠ 0:
    if ∃ i such that lt(gi) | lt(p):
        choose the smallest such i
        qi ← qi + lt(p) / lt(gi)
        p ← p - (lt(p) / lt(gi)) · gi
    else:
        r ← r + lt(p)
        p ← p - lt(p)
return (q1, ..., qs, r)

Key properties:

  1. Termination: guaranteed by the well-ordering of the monomial order
  2. Non-uniqueness: the remainder $r$ depends on the order of division $g_1, \ldots, g_s$. Different orders may produce different remainders
  3. The key role of Gröbner bases: when ${g_1, \ldots, g_s}$ forms a Gröbner basis, the remainder $r$ is unique — this is one of the core values of Gröbner bases

Example: Take $f = x^2 y + x y^2 + y^2$, $g_1 = xy - 1$, $g_2 = y^2 - 1$, using the Grevlex order.

Step 1: $\text{lt}(f) = x^2 y$, and $\text{lt}(g_1) = xy$ divides $x^2 y$.

$$f \leftarrow f - x \cdot g_1 = x^2 y + x y^2 + y^2 - x(xy - 1) = x y^2 + y^2 + x$$

Step 2: $\text{lt}(p) = x y^2$, and $\text{lt}(g_1) = xy$ divides $x y^2$.

$$p \leftarrow x y^2 + y^2 + x - y \cdot g_1 = x y^2 + y^2 + x - y(xy - 1) = y^2 + x + y$$

Step 3: $\text{lt}(p) = y^2$, and $\text{lt}(g_2) = y^2$ divides $y^2$.

$$p \leftarrow y^2 + x + y - 1 \cdot g_2 = y^2 + x + y - (y^2 - 1) = x + y + 1$$

At this point $\text{lt}(p) = x$ is not divisible by $xy$ or $y^2$, so it moves into the remainder: $r = x + y + 1$.

S-Polynomials

The S-polynomial is the core building block of Gröbner basis algorithms.

Definition. Let $f, g$ be nonzero polynomials and $\gamma = \text{lcm}(\text{lm}(f), \text{lm}(g))$ (componentwise maximum of the exponent vectors). The S-polynomial of $f$ and $g$ is

$$S(f, g) = \frac{\gamma}{\text{lt}(f)} \cdot f - \frac{\gamma}{\text{lt}(g)} \cdot g = \frac{x^\gamma}{\text{lm}(f)} \cdot f - \frac{x^\gamma}{\text{lm}(g)} \cdot g$$

The intuition behind the S-polynomial is "canceling the leading terms of $f$ and $g$". The Buchberger algorithm repeatedly constructs S-polynomials and reduces them until all S-polynomials reduce to zero — the resulting polynomial set is then a Gröbner basis.

Dickson's Lemma

Dickson's lemma is a finite-generation theorem for monomial ideals and the foundation of Hilbert's basis theorem.

Lemma (Dickson). Every subset $S$ of $\mathbb{N}^n$ has a finite set of "minimal elements" $M \subseteq S$ such that for every $\alpha \in S$ there exists $\beta \in M$ with $\beta \leq \alpha$ (componentwise).

Equivalent statement: every ideal of $\mathbb{F}[x_1, \ldots, x_n]$ generated by monomials is generated by finitely many monomials.

Intuition: In $\mathbb{N}^n$, a "downward-closed" set can only be generated by finitely many "minimal elements". This is analogous to well-ordering in $\mathbb{N}$, but generalized to higher dimensions.

Hilbert's Basis Theorem

Theorem (Hilbert's basis theorem). Every ideal $I$ of $\mathbb{F}[x_1, \ldots, x_n]$ is finitely generated. That is, there exist $f_1, \ldots, f_s \in I$ such that $I = \langle f_1, \ldots, f_s \rangle$.

Proof sketch: Let $J = \langle f_1, \ldots, f_s \rangle$. Consider the leading-term ideal $L(I) = \langle \text{lm}(f) : f \in I \rangle$. By Dickson's lemma, $L(I) = \langle \text{lm}(f_1), \ldots, \text{lm}(f_s) \rangle$, hence $L(J) \supseteq L(I)$. If $J \subsetneq I$, pick $f \in I \setminus J$ whose leading monomial $\text{lm}(f)$ is minimal with respect to $\succ$. Since $\text{lm}(f) \in L(I) \subseteq L(J)$, the defining property of monomial ideals yields some $f_i$ with $\text{lm}(f_i) \mid \text{lm}(f)$; eliminating the leading term of $f$ with $f_i$ gives $f' = f - c, \mathbf{x}^\gamma f_i \in I \setminus J$ with $\text{lm}(f') \prec \text{lm}(f)$ — contradicting the minimality of $\text{lm}(f)$ (well-ordering forbids infinite descending chains). Hence $J = I$.

Significance: Hilbert's basis theorem guarantees the existence of Gröbner bases — the Gröbner basis of every ideal is a finite set. The Buchberger algorithm further provides a constructive proof.

Implementation in oCAS

Dense Univariate Polynomials

DenseUnivariatePolynomial<D> stores the coefficients from the constant term to the leading coefficient in one contiguous vector:

#![allow(unused)]
fn main() {
pub struct DenseUnivariatePolynomial<D: Domain> {
    coeffs: Vec<D::Element>,  // [a0, a1, ..., an]
    domain: D,
}
}

Design decisions:

  • Trailing-zero trimming: trailing zero coefficients are removed automatically at construction; the zero polynomial is represented by the empty vector
  • Inlined coefficient domain: the domain field is stored inside the polynomial, avoiding the need to pass it to every operation
  • Polynomial type parameter: the D: Domain generic allows a unified implementation over the integer ring $\mathbb{Z}$, the rational field $\mathbb{Q}$, finite fields $\mathbb{F}_p$, and so on

Multiplication strategies:

ConditionAlgorithmComplexity
$\min(\deg f, \deg g) + 1 < 32$ (number of coefficients)Schoolbook (term-by-term multiplication)$O(n \cdot m)$
$\min(\deg f, \deg g) + 1 \geq 32$Karatsuba$O(n^{1.585})$
Finite field + NTT-friendly prime + $\geq 256$ coefficients (mul_ntt)Number theoretic transform$O(n \log n)$

Note: the generic mul takes one of the first two rows (Karatsuba/Schoolbook; the threshold KARATSUBA_THRESHOLD = 32 is applied to the number of coefficients, i.e. degree $\geq 31$). The NTT is enabled through DenseUnivariatePolynomial<FiniteField>::mul_ntt, which internally checks NTT_THRESHOLD = 256 (number of coefficients) and whether the prime is NTT-friendly, and otherwise falls back to Karatsuba/Schoolbook.

The core idea of Karatsuba is to split the polynomials at the midpoint $m = n/2$:

$$a = a_0 + a_1 x^m, \quad b = b_0 + b_1 x^m$$

The product is computed via three half-size multiplications (instead of four):

$$z_0 = a_0 b_0, \quad z_2 = a_1 b_1, \quad z_1 = (a_0 + a_1)(b_0 + b_1) - z_0 - z_2$$

$$a \cdot b = z_0 + z_1 x^m + z_2 x^{2m}$$

EuclideanDomain methods (available when $D: \text{EuclideanDomain}$):

  • div_rem(&self, divisor) — division with remainder, returns Option<(quotient, remainder)>
  • gcd(&self, other) — polynomial GCD (via a pseudo-remainder PRS chain)
  • extended_gcd_poly(&self, other) — extended GCD, returns $(g, s, t)$ with $s \cdot f + t \cdot g = \text{gcd}(f, g)$
  • content() — the content (GCD of all coefficients)
  • primitive_part() — the primitive part $f / \text{content}(f)$
  • square_free_factorization() — square-free factorization
  • resultant(&self, other) — the resultant (Brown's PRS algorithm)
  • pow(n) — fast exponentiation (repeated squaring)
  • p_adic_expansion(p) — the $p$-adic expansion

Univariate polynomial evaluation uses Horner's rule, in $O(n)$ time:

$$f(x) = a_0 + x(a_1 + x(a_2 + \cdots + x \cdot a_n))$$

#![allow(unused)]
fn main() {
// Horner's method
let mut result = domain.zero();
for coeff in self.coeffs.iter().rev() {
    result = domain.mul(&result, x);
    result = domain.add(&result, coeff);
}
}

Sparse Multivariate Polynomials

SparseMultivariatePolynomial<D, O> stores only nonzero terms, mapping exponent vectors to coefficients with a HashMap:

#![allow(unused)]
fn main() {
pub struct SparseMultivariatePolynomial<D: Domain, O: MonomialOrder = Grevlex> {
    terms: HashMap<SmallVec<[usize; 4]>, D::Element>,
    domain: D,
    n_vars: usize,
    pub order: O,
}
}

Design points:

  • Exponent vectors: SmallVec<[usize; 4]> — stored inline for small variable counts (no heap allocation for ≤ 4 variables), spilling to the heap for large variable counts
  • Default order: O = Grevlex — the most commonly used order for Gröbner basis computations, since it produces the smallest intermediate expressions
  • Order as a type parameter: the monomial order is determined at compile time, a zero-cost abstraction. Lex, Grlex, Grevlex are zero-sized types (ZSTs) occupying no runtime space; WeightOrder, MatrixOrder, BlockOrder carry runtime configuration

The MonomialOrder trait:

#![allow(unused)]
fn main() {
pub trait MonomialOrder: Clone + PartialEq + Eq + Debug + Default {
    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> std::cmp::Ordering;
}
}

The single method cmp compares two exponent vectors. The implementations for each order:

#![allow(unused)]
fn main() {
// Lexicographic order: compare exponent vectors directly
impl MonomialOrder for Lex {
    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> Ordering {
        lhs.cmp(rhs)
    }
}

// Graded reverse lexicographic: total degree first (descending), then reverse lex
impl MonomialOrder for Grevlex {
    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> Ordering {
        let deg_lhs: usize = lhs.iter().sum();
        let deg_rhs: usize = rhs.iter().sum();
        deg_rhs.cmp(&deg_lhs)
            .then_with(|| rhs.iter().rev().cmp(lhs.iter().rev()))
    }
}

// Graded lexicographic: total degree first (descending), then lex
impl MonomialOrder for Grlex {
    fn cmp(&self, lhs: &[usize], rhs: &[usize]) -> Ordering {
        let deg_lhs: usize = lhs.iter().sum();
        let deg_rhs: usize = rhs.iter().sum();
        deg_rhs.cmp(&deg_lhs)
            .then_with(|| lhs.cmp(rhs))
    }
}
}

Implementation detail: deg_rhs.cmp(&deg_lhs) rather than deg_lhs.cmp(&deg_rhs) — because in Rust's Ordering, Less means "comes first", and we need the higher degree to come first (i.e. Less), so we compare deg_rhs against deg_lhs. Similarly, in Grevlex, rhs.iter().rev().cmp(lhs.iter().rev()) implements the "reverse" lexicographic order.

Multivariate division is implemented via reduce(&self, basis: &[Self]):

#![allow(unused)]
fn main() {
pub fn reduce(&self, basis: &[Self]) -> Self {
    let mut remainder = self.clone();
    let mut result = self.zero();

    // Cache each basis element's leading term.
    let basis_lts: Vec<_> = basis
        .iter()
        .filter_map(|g| g.leading_term().map(|(e, c)| (g, e.clone(), c.clone())))
        .collect();

    let max_iter = 10000;
    for _ in 0..max_iter {
        if remainder.is_zero() {
            break;
        }
        let (rm, rc) = match remainder.leading_term() {
            Some((e, c)) => (e, c),
            None => break,
        };

        let mut reduced = false;
        for (g, lm, lc) in &basis_lts {
            // lm | rm (componentwise rm[i] ≥ lm[i])
            if monomial_divides(&rm, lm) {
                // compute the quotient monomial
                let qm: SmallVec<[usize; 4]> =
                    rm.iter().zip(lm.iter()).map(|(a, b)| a - b).collect();
                let qc = match self.domain.div(&rc, lc) {
                    Some(q) => q,
                    None => break,
                };
                remainder = remainder.sub(&g.mul_monomial(&qm).mul_scalar(&qc));
                reduced = true;
                break;
            }
        }

        if !reduced {
            result.terms.insert(rm, rc);
            remainder.terms.remove(&rm);
        }
    }
    result
}
}

Helper functions (monomial operations):

#![allow(unused)]
fn main() {
// whether x^b divides x^a (componentwise a[i] ≥ b[i])
pub fn monomial_divides(a: &[usize], b: &[usize]) -> bool

// least common multiple monomial (componentwise max)
pub fn monomial_lcm(a: &[usize], b: &[usize]) -> SmallVec<[usize; 4]>

// whether two monomials are coprime (no common variable)
pub fn monomial_are_coprime(a: &[usize], b: &[usize]) -> bool
}

Dense vs. Sparse: A Selection Guide

ConsiderationDense DenseUnivariatePolynomialSparse SparseMultivariatePolynomial
Number of variablesUnivariateMultivariate ($n$ variables)
StorageContiguous Vec, $O(\deg f + 1)$HashMap, $O(\text{number of nonzero terms})$
MultiplicationKaratsuba / NTTTerm-by-term expansion
DivisionUnique quotient and remainderDepends on the monomial order; remainder not unique
GCDEuclid's algorithm / PRSRequires Gröbner basis techniques
Supported coefficient domainsEuclideanDomainDomain (division requires a field)

Rule of thumb: use the dense representation for univariate polynomials; use the sparse representation for two or more variables. Even a multivariate polynomial with only a few nonzero terms (such as $x^{100} + y^{100}$) needs only two stored terms in the sparse representation, whereas the dense representation would require a $100 \times 100$ coefficient array.

Advanced Topics

Equivalence and Unification of Monomial Orders

Every standard monomial order can be represented uniformly as a matrix order. For $n$ variables:

  • Lex $=$ $M = I_n$ (the identity matrix)
  • Grlex $=$ the first row all ones, the remaining rows the lexicographic unit vectors in order ($e_1, e_2, \ldots, e_n$ — total degree first, then compare components left to right)
  • Grevlex $=$ the first row all ones, the remaining rows the negative reverse-lexicographic unit vectors ($-e_n, -e_{n-1}, \ldots, -e_1$ — total degree first, then compare right to left, the smaller one being greater)

The elimination order constructed by MatrixOrder::elimination_order(k, n) is equivalent to a BlockOrder where the first $k$ variables use Lex and the rest use Grevlex, but the matrix form is more concise.

Limitations of Division

One important limitation of multivariate division with remainder: the remainder depends on the order of the divisors. For example, dividing $f$ by $(g_1, g_2)$ and by $(g_2, g_1)$ may produce different remainders. The introduction of Gröbner bases precisely solves this problem — division by a Gröbner basis yields a unique remainder.

Polynomials over the Integers

When the coefficient ring is $\mathbb{Z}$ (rather than a field), division with remainder needs adjustment. The div_rem of DenseUnivariatePolynomial<IntegerDomain> implements pseudo-division by performing integer division with remainder on the leading coefficients. Concretely, for $f = q \cdot g + r$, the coefficients of $q$ and $r$ may be larger than those of $f$, because division in $\mathbb{Z}$ is not always exact. GCD computation instead uses a pseudo-remainder sequence (PRS) to avoid fraction explosion; for very large integer coefficients the modular GCD can be used instead (gcd/modular.rs, Brown's algorithm). The subresultant PRS is used for resultant computation (resultant.rs).

References

  1. Cox, D., Little, J., O'Shea, D. Ideals, Varieties, and Algorithms (4th ed.). Springer, 2015.

    • Chapter 1: basic properties of polynomial rings
    • Chapter 2: introduction to Gröbner bases and the Buchberger algorithm
  2. Gathen, J. von zur, Gerhard, J. Modern Computer Algebra (3rd ed.). Cambridge University Press, 2013.

    • Chapter 6: polynomial multiplication algorithms (Karatsuba, FFT, NTT)
    • Chapter 21: multivariate polynomial GCD
  3. Adams, W. W., Loustaunau, P. An Introduction to Gröbner Bases. AMS, 1994.

    • Chapter 1: monomial orders and multivariate division

See also

Foundations: Finite Fields and Modular Arithmetic

Prerequisites

  • Set theory and basic algebraic structures (definitions of groups, rings, and fields)
  • Integer division and remainders
  • Basic polynomial operations

Suggested reading: Polynomial Algebra, Linear Algebra.


Basic Concepts

Modular Arithmetic

Given a positive integer $n \geq 2$, the integers $a$ and $b$ are congruent modulo $n$ (written $a \equiv b \pmod{n}$) if and only if $n \mid (a - b)$. Modular arithmetic partitions the integers into $n$ equivalence classes:

$$ \mathbb{Z}/n\mathbb{Z} = {\overline{0},, \overline{1},, \dots,, \overline{n-1}} $$

where $\overline{a} = {a + kn \mid k \in \mathbb{Z}}$. Addition and multiplication are performed on representatives and the result is then reduced modulo $n$:

$$ \overline{a} + \overline{b} = \overline{a + b}, \quad \overline{a} \cdot \overline{b} = \overline{a \cdot b} $$

This makes $\mathbb{Z}/n\mathbb{Z}$ a commutative ring.

Modular Inverses and the Extended Euclid Algorithm

The element $\overline{a} \in \mathbb{Z}/n\mathbb{Z}$ has a multiplicative inverse if and only if $\gcd(a, n) = 1$. In that case there exist integers $x, y$ satisfying the Bézout identity:

$$ ax + ny = \gcd(a, n) = 1 $$

Reducing modulo $n$ gives $ax \equiv 1 \pmod{n}$, hence $\overline{x} = \overline{a}^{-1}$.

The extended Euclid algorithm solves this identity iteratively:

  1. Initialize: $(r_0, r_1) = (a, n)$, $(s_0, s_1) = (1, 0)$, $(t_0, t_1) = (0, 1)$.
  2. At each step compute $q = \lfloor r_{i-1}/r_i \rfloor$ and update:

$$ \begin{pmatrix} r_{i+1} \ s_{i+1} \ t_{i+1} \end{pmatrix} = \begin{pmatrix} r_{i-1} - q \cdot r_i \ s_{i-1} - q \cdot s_i \ t_{i-1} - q \cdot t_i \end{pmatrix} $$

  1. Terminate when $r_{i+1} = 0$; then $r_i = \gcd(a, n)$ and $s_i \cdot a + t_i \cdot n = r_i$.

Prime Fields $\mathbb{Z}/p\mathbb{Z}$

When $n = p$ is prime, the structure of $\mathbb{Z}/p\mathbb{Z}$ changes fundamentally: every nonzero element is coprime to $p$ and therefore has an inverse. This makes $\mathbb{Z}/p\mathbb{Z}$ a field, denoted $\mathbb{F}_p$.

Theorem (Fermat's Little Theorem): let $p$ be prime and $a \not\equiv 0 \pmod{p}$; then

$$ a^{p-1} \equiv 1 \pmod{p} $$

It follows immediately that $a^{p-2} \equiv a^{-1} \pmod{p}$, which provides an implementation of finite-field inversion that does not need the extended Euclid algorithm — a single modular exponentiation suffices. With the pow_mod of the GMP backend this is much faster than repeated division.

Basic properties of fields:

  • The additive group $(\mathbb{F}_p, +)$ is cyclic of order $p$, isomorphic to $(\mathbb{Z}/p\mathbb{Z}, +)$.
  • The multiplicative group $(\mathbb{F}_p^, \times)$ is cyclic of order $p-1$ — there exists a primitive root $g$ such that ${g^0, g^1, \dots, g^{p-2}} = \mathbb{F}_p^$.
  • The characteristic is $p$: $p \cdot 1 = \underbrace{1 + \cdots + 1}_{p} = 0$, and $p$ is the smallest positive integer with this property.

Core Theory

Construction of Finite Fields $\mathbb{F}_{p^d}$

The prime field $\mathbb{F}_p$ is not the only finite field. The classification theorem for finite fields states that for every prime power $q = p^d$ ($d \geq 1$) there exists a unique (up to isomorphism) finite field $\mathbb{F}_q$ with $q$ elements, and every finite field is of this form.

The standard way to construct $\mathbb{F}_{p^d}$ ($d > 1$) is by irreducible polynomial extension:

  1. Pick a degree-$d$ irreducible polynomial $m(x)$ in $\mathbb{F}_p[x]$.
  2. Define $\mathbb{F}_{p^d} = \mathbb{F}_p[x] / (m(x))$ — the quotient of the polynomial ring by the ideal $(m(x))$.
  3. Elements are polynomials $a_0 + a_1 \alpha + \cdots + a_{d-1}\alpha^{d-1}$ of degree $< d$, where $\alpha = \overline{x}$ is a root of $m$.
  4. Addition is coefficient-wise modulo $p$; multiplication is polynomial multiplication followed by reduction modulo $m(x)$.

Existence of irreducible polynomials: the number of monic degree-$d$ irreducible polynomials in $\mathbb{F}_p[x]$ is

$$ N_p(d) = \frac{1}{d}\sum_{k \mid d} \mu(k) \cdot p^{d/k} $$

where $\mu$ is the Möbius function. This guarantees that irreducible polynomials exist for every $d \geq 1$.

Example: $\mathbb{F}_4 = \mathbb{F}_2[x]/(x^2 + x + 1)$. Let $\alpha$ be a root of $x^2 + x + 1$; then $\alpha^2 = \alpha + 1$ (in $\mathbb{F}_2$, $-1 = 1$). The four elements are ${0, 1, \alpha, \alpha+1}$.

Cyclic Multiplicative Groups

Theorem: the multiplicative group $\mathbb{F}_{p^d}^*$ of a finite field is cyclic, of order $p^d - 1$.

This means there exists a primitive element $g \in \mathbb{F}_{p^d}^*$ such that

$$ \mathbb{F}_{p^d}^* = {g^0, g^1, \dots, g^{p^d - 2}} $$

Cyclicity has far-reaching applications in cryptography and coding theory: the discrete logarithm problem (DLP), pseudorandom number generation, and the construction of error-correcting codes all rely on it.

Characteristic and Prime Subfield

The characteristic of the finite field $\mathbb{F}_{p^d}$ is $p$. From the definition of the characteristic:

  • $p \cdot a = 0$ for all $a \in \mathbb{F}_{p^d}$.
  • $\mathbb{F}p$ is the prime subfield of $\mathbb{F}{p^d}$, i.e. the smallest subfield containing $1$.
  • As an $\mathbb{F}p$-vector space, $\mathbb{F}{p^d}$ has dimension $d$.

The Frobenius endomorphism $\varphi: x \mapsto x^p$ is an important structural map on $\mathbb{F}_{p^d}$. Its $d$-fold iterate satisfies $\varphi^d = \mathrm{id}$, and its fixed points are exactly the elements of $\mathbb{F}_p$.


Implementation in oCAS

oCAS's finite-field implementation lives in ocas-domain/src/finite_field.rs and supports prime fields $\mathbb{Z}/p\mathbb{Z}$. Extension fields $\mathbb{F}_{p^d}$ are built via AlgebraicExtension<FiniteField> (see ocas-domain/src/algebraic.rs).

Core Structs

FiniteField represents a prime field $\mathbb{F}_p$:

#![allow(unused)]
fn main() {
pub struct FiniteField {
    prime: BigInt,
    prime_minus_two: BigInt,  // cached p-2, used for Fermat inversion
    #[cfg(feature = "gmp")]
    prime_gmp: rug::Integer,  // GMP backend to accelerate modular arithmetic
    #[cfg(feature = "gmp")]
    prime_minus_two_gmp: rug::Integer,
}
}
  • The prime field stores the modulus $p$ as a num-bigint arbitrary-precision integer.
  • prime_minus_two is precomputed at construction time so it does not have to be recomputed on every inversion.
  • With the gmp feature enabled, the GMP representations are cached as well, exploiting rug's native modular exponentiation.

FiniteFieldElement represents an element of the field:

#![allow(unused)]
fn main() {
pub struct FiniteFieldElement {
    value: BigInt,  // always kept in the range [0, p-1]
}
}

Elements are always stored as their canonical representative — a nonnegative integer in the range $[0, p-1]$. This guarantees that semantic equality coincides with structural equality (PartialEq compares the values directly) without reducing modulo $p$ on every comparison.

Construction and Element Creation

#![allow(unused)]
fn main() {
// create F_7
let f = FiniteField::new(BigInt::from(7));

// create an element from an arbitrary integer (automatically reduced to [0, p-1])
let a = f.element(10);   // a = 10 mod 7 = 3
let b = f.element(-3);   // b = -3 mod 7 = 4
}

The element() method uses mod_floor (rather than plain %), ensuring that negative numbers are also mapped correctly into $[0, p-1]$.

Arithmetic Operations

All operations go through the uniform interface of the Domain trait:

OperationMethodImplementation strategy
Additionf.add(&a, &b)$(a + b) \bmod p$
Subtractionf.sub(&a, &b)$(a - b) \bmod p$ (mod_floor handles negatives)
Multiplicationf.mul(&a, &b)$(a \cdot b) \bmod p$
Negationf.neg(&a)$(-a) \bmod p$
Inversionf.inv(&a)$a^{p-2} \bmod p$ (Fermat's little theorem); None when $a = 0$
Divisionf.div(&a, &b)$a \cdot b^{-1}$; None when $b = 0$
Exponentiationf.pow(&a, n)fast modular exponentiation (binary exponentiation)

Inversion implementation details:

#![allow(unused)]
fn main() {
fn inv(&self, a: &FiniteFieldElement) -> Option<FiniteFieldElement> {
    if a.value.is_zero() {
        return None;  // zero has no inverse
    }
    // Fermat: a^(p-2) ≡ a^{-1} (mod p)
    Some(self.normalize(a.value.modpow(&self.prime_minus_two, &self.prime)))
}
}

Fermat's little theorem is used instead of the extended Euclid algorithm because binary exponentiation via modpow needs only $O(\log p)$ multiplications and is simple to implement. With the GMP backend enabled, rug::Integer::pow_mod uses GMP's highly optimized implementation underneath.

The EuclideanDomain Implementation

Finite fields also implement the EuclideanDomain trait, but Euclidean division over a field is degenerate:

#![allow(unused)]
fn main() {
// division over a field is always exact; the remainder is always zero
fn div_rem(&self, a, b) -> Option<(Self::Element, Self::Element)> {
    self.div(a, b).map(|q| (q, self.zero()))
}

// GCD over a field: returns 0 when both are zero, otherwise 1
fn gcd(&self, a, b) -> Self::Element {
    if self.is_zero(a) && self.is_zero(b) {
        self.zero()
    } else {
        self.one()
    }
}
}

This is because every nonzero element of a field is a unit, so the notion of GCD degenerates to a trivial case.

Extension Fields $\mathbb{F}_{p^d}$

Constructed via AlgebraicExtension<FiniteField>:

#![allow(unused)]
fn main() {
use ocas_domain::{AlgebraicExtension, Domain, FiniteField};
use num_bigint::BigInt;

// construct F_4 = F_2[x]/(x^2 + x + 1)
let f2 = FiniteField::new(BigInt::from(2));
let f4 = AlgebraicExtension::new(
    f2,
    vec![
        f2.element(1),  // constant term
        f2.element(1),  // x coefficient
        f2.element(1),  // x^2 coefficient (leading term)
    ],
);
let alpha = f4.alpha();  // α = x mod (x^2+x+1)
// α^2 = α + 1 (in F_2)
let alpha_sq = f4.mul(&alpha, &alpha);
}

Internally, AlgebraicElement<E> stores a vector of coefficients in ascending powers, with trailing zeros trimmed:

#![allow(unused)]
fn main() {
pub struct AlgebraicElement<E> {
    coeffs: Vec<E>,  // [a_0, a_1, ..., a_{d-1}] represents a_0 + a_1*α + ...
}
}

Inversion uses the extended Euclid algorithm (a self-contained implementation over dense polynomials of the base field): it computes $s(x) \cdot a(x) + t(x) \cdot m(x) = 1$, so that $s(x) \bmod m(x) = a(x)^{-1}$.


Advanced Topics

Number-Theoretic Transforms (NTT) and Fast Polynomial Multiplication

The NTT is the finite-field analogue of the FFT. Standard FFT uses the roots of unity $\omega_n = e^{2\pi i/n}$ over the complex numbers $\mathbb{C}$; the NTT instead uses $n$-th roots of unity modulo $p$ in $\mathbb{F}_p$.

Existence condition: $\mathbb{F}_p$ contains a primitive $n$-th root of unity if and only if $n \mid (p - 1)$. This is because $\mathbb{F}_p^*$ is a cyclic group of order $p-1$, and an $n$-th root of unity exists $\iff$ $n$ divides the group order.

Algorithm (given $f, g \in \mathbb{F}_p[x]$, each of degree $< n$):

  1. Zero padding: pad the coefficient vectors of $f, g$ to length $N = 2^{\lceil \log_2(2n) \rceil}$.
  2. Forward transform: $\hat{f} = \mathrm{NTT}(f)$, $\hat{g} = \mathrm{NTT}(g)$.
  3. Pointwise multiplication: $\hat{h}_i = \hat{f}_i \cdot \hat{g}_i$ (component-wise modulo $p$).
  4. Inverse transform: $h = \mathrm{NTT}^{-1}(\hat{h})$, then multiply by $N^{-1} \bmod p$.

Total complexity is $O(N \log N)$, compared with $O(n^2)$ for naive convolution and $O(n^{1.585})$ for Karatsuba.

The NTT implementation in oCAS (ocas-poly/src/ntt.rs):

  • Algorithm: Cooley–Tukey radix-2 DIT (decimation in time) with bit-reversal permutation.
  • Montgomery arithmetic: all intermediate values are stored in Montgomery form $x \cdot R \bmod p$ ($R = 2^{64}$) to avoid expensive u128 % p operations. One conversion is performed at the entry and one at the exit.
  • Trigger condition: activated automatically when the number of coefficients is $\geq 256$ (NTT_THRESHOLD, i.e. degree $\geq 255$) and $p$ is NTT-friendly ($p-1$ contains a sufficiently large power of 2); otherwise it falls back to Karatsuba.
  • Limitation: currently only primes representable in a u64 are supported ($p < 2^{64}$, checked in try_ntt_mul_fp); since Montgomery reduction uses 128-bit intermediates, $p \leq 2^{63}$ guarantees no overflow.
#![allow(unused)]
fn main() {
// NTT-friendly check
pub fn is_ntt_friendly(p: u64, n: usize) -> bool {
    // n must divide p-1
    if n == 0 { return true; }
    let pm1 = p - 1;
    pm1 % (n as u64) == 0
}
}

Construction of NTT-friendly primes: in practice one uses primes of the form $p = k \cdot 2^m + 1$ (e.g. the NTT prime $998244353 = 119 \cdot 2^{23} + 1$), which guarantees $2^{23} \mid (p-1)$ and hence supports transforms of length up to $2^{23}$.

Montgomery multiplication replaces modular reduction with multiplications and shifts:

$$ \mathrm{MontMul}(a, b) = a \cdot b \cdot R^{-1} \bmod p $$

where $R = 2^{64}$, and $R^{-1} \bmod p$ together with $p' = -p^{-1} \bmod R$ are precomputed. The multiplication steps are:

  1. $t = a \cdot b$ (128-bit intermediate value)
  2. $m = (t \bmod R) \cdot p' \bmod R$
  3. $u = (t + m \cdot p) / R$
  4. if $u \geq p$ then $u = u - p$

Only additions, multiplications, and shifts are used throughout — no expensive division.

NTT in Polynomial GCD

The NTT does not only accelerate multiplication; it also indirectly speeds up polynomial GCD and factorization. During Hensel lifting, multiplying the many small-coefficient polynomials is the bottleneck, and the NTT reduces it from $O(n^2)$ to $O(n \log n)$. oCAS's DenseUnivariatePolynomial<FiniteField>::mul_ntt automatically selects the NTT when the number of coefficients is $\geq 256$ and the prime is NTT-friendly (otherwise it falls back to Karatsuba/Schoolbook); the generic mul still uses the Karatsuba/Schoolbook path.


References

  1. Shoup, V. A Computational Introduction to Number Theory and Algebra. Cambridge University Press, 2nd edition, 2009. — Chapters 4–5 cover finite-field construction, cyclic groups, and irreducible polynomials.
  2. Lidl, R. & Niederreiter, H. Introduction to Finite Fields and Their Applications. Cambridge University Press, 1994. — The classical textbook on finite-field theory.
  3. Gathen, J. von zur & Gerhard, J. Modern Computer Algebra. Cambridge University Press, 3rd edition, 2013. — Chapter 8 (fast polynomial multiplication and the NTT) and Chapter 14 (finite-field arithmetic).
  4. Menezes, A., van Oorschot, P. & Vanstone, S. Handbook of Applied Cryptography. CRC Press, 1996. — Chapter 2 covers modular arithmetic and the extended Euclid algorithm.

Foundations: Linear Algebra

This chapter presents the mathematical foundations of matrix operations in oCAS. The Matrix type in oCAS works over a EuclideanDomain — not just floats or rationals, but any algebraic structure supporting Euclidean division. This means we need an elimination algorithm that does not introduce fractions.


Prerequisites

Before reading this chapter, you should be familiar with:

  • Basic notions of rings and fields (see Finite Fields and Modular Arithmetic)
  • Euclidean domains: algebraic structures supporting add, mul, sub, neg, div (division with remainder), gcd, and rem
  • Basic intuition for vector spaces: vector addition and scalar multiplication

No systematic linear algebra training is required — this chapter builds everything from scratch.


Basic Concepts

Matrices

An $m \times n$ matrix is a rectangular array of elements arranged in $m$ rows and $n$ columns:

$$ A = \begin{pmatrix} a_{11} & a_{12} & \cdots & a_{1n} \ a_{21} & a_{22} & \cdots & a_{2n} \ \vdots & \vdots & \ddots & \vdots \ a_{m1} & a_{m2} & \cdots & a_{mn} \end{pmatrix} $$

The element $a_{ij}$ sits in row $i$ and column $j$. In oCAS, elements are stored in a row-major one-dimensional array: data[i * ncols + j] corresponds to $a_{i+1,,j+1}$ (zero-indexed).

Matrix Operations

Addition: two matrices of the same shape are added elementwise:

$$ (A + B){ij} = a{ij} + b_{ij} $$

Scalar multiplication:

$$ (cA){ij} = c \cdot a{ij} $$

Matrix multiplication: the product $C = AB$ of an $m \times n$ matrix $A$ and an $n \times p$ matrix $B$ is the $m \times p$ matrix:

$$ c_{ij} = \sum_{k=1}^{n} a_{ik} \cdot b_{kj} $$

Multiplication requires the number of columns of $A$ to equal the number of rows of $B$. Note that matrix multiplication is generally not commutative ($AB \neq BA$), but it is associative ($(AB)C = A(BC)$).

Transpose: swapping rows and columns:

$$ (A^\top){ij} = a{ji} $$

Properties: $(AB)^\top = B^\top A^\top$, $(A^\top)^\top = A$.

Trace: the sum of the diagonal elements of a square matrix:

$$ \operatorname{tr}(A) = \sum_{i=1}^{n} a_{ii} $$

Property: $\operatorname{tr}(AB) = \operatorname{tr}(BA)$ (whenever the product is defined).

Determinants

The determinant is a scalar value attached to a square matrix, one of the most central concepts in linear algebra. For an $n \times n$ matrix $A$, the determinant $\det(A)$ has the following equivalent definitions:

Recursive definition (Laplace expansion): expanding along row $i$:

$$ \det(A) = \sum_{j=1}^{n} (-1)^{i+j} , a_{ij} , M_{ij} $$

where $M_{ij}$ is the determinant of the $(n-1)\times(n-1)$ submatrix obtained by deleting row $i$ and column $j$ (the minor). For a $1\times 1$ matrix, $\det(a_{11}) = a_{11}$.

Geometric definition: $|\det(A)|$ is the "oriented volume" of the parallelepiped spanned by the column vectors of the matrix. $\det(A) > 0$ means orientation is preserved; $\det(A) < 0$ means orientation is flipped.

Key Properties of Determinants

  1. Multiplicativity: $\det(AB) = \det(A) \cdot \det(B)$
  2. Transpose invariance: $\det(A^\top) = \det(A)$
  3. Row swap changes sign: swapping two rows changes the sign of the determinant
  4. Row scaling: multiplying a row by $c$ multiplies the determinant by $c$
  5. Row addition invariance: adding a multiple of one row to another leaves the determinant unchanged
  6. Singularity test: $\det(A) = 0$ if and only if $A$ is not invertible (singular)
  7. Upper/lower triangular matrices: $\det(A) = \prod_{i=1}^{n} a_{ii}$ (the product of the diagonal elements)

These properties directly suggest computing determinants by elimination: transform the matrix into triangular form by row operations, and the determinant is the product of the diagonal elements (times a sign factor from row swaps).


Core Theory

The Problem: Fraction Explosion over Domains

Classical Gaussian elimination works well over the rationals — we can divide freely. But Matrix<D: EuclideanDomain> in oCAS is required to work over arbitrary Euclidean domains, including:

  • The integer ring $\mathbb{Z}$: $3 \div 2$ is meaningless (no integer quotient)
  • Polynomial rings $\mathbb{F}[x]$: $x^2 \div (x+1)$ is not a polynomial
  • Finite fields $\mathbb{F}_p$: division is possible, but every division incurs a modular inverse, and intermediate results may overflow

Even over the rationals, classical elimination produces ever-growing denominators. For example, for the integer matrix:

$$ A = \begin{pmatrix} 2 & 3 \ 5 & 7 \end{pmatrix} $$

classical elimination needs to compute $7 - \frac{5 \cdot 3}{2} = 7 - \frac{15}{2} = -\frac{1}{2}$ — a fraction appears. For an $n \times n$ matrix, the numerators and denominators of the fractions can grow to magnitude $O(n!)$.

Bareiss's Fraction-Free Determinant Algorithm

Bareiss's algorithm (1968) is built on the key insight that during elimination over the integers (or a more general Euclidean domain), all intermediate values can be kept integral by dividing by the previous pivot.

Algorithm Description

For an $n \times n$ matrix $A$, Bareiss's algorithm produces an upper triangular matrix whose last diagonal element $a_{nn}^{(n-1)}$ is $\det(A)$ (possibly multiplied by $-1$ to compensate for row swaps).

Initialization: set $a_{ij}^{(0)} = a_{ij}$ and $p_0 = 1$.

For $k = 0, 1, \ldots, n-2$:

  1. Pivot selection: if $a_{kk}^{(k)} = 0$, find $i > k$ with $a_{ik}^{(k)} \neq 0$, and swap row $k$ with row $i$ (record the sign flip). If none exists, $\det(A) = 0$.

  2. Elimination: for $i = k+1, \ldots, n-1$ and $j = k+1, \ldots, n-1$:

$$ a_{ij}^{(k+1)} = \frac{a_{ij}^{(k)} \cdot a_{kk}^{(k)} - a_{ik}^{(k)} \cdot a_{kj}^{(k)}}{p_k} $$

  1. Update the divisor: $p_{k+1} = a_{kk}^{(k)}$

Finally:

$$ \det(A) = \begin{cases} a_{nn}^{(n-1)} & \text{if no row swap or an even number of swaps} \ -a_{nn}^{(n-1)} & \text{if an odd number of row swaps} \end{cases} $$

Why the Division Is Exact

Theorem (Bareiss 1968): in the formula above, $p_k$ divides the numerator $a_{ij}^{(k)} \cdot a_{kk}^{(k)} - a_{ik}^{(k)} \cdot a_{kj}^{(k)}$ exactly.

Intuitively, this follows from Sylvester's determinant identity. The step-$(k+1)$ update of the elimination matrix is equivalent to computing the determinant of a $(k+2) \times (k+2)$ submatrix, and the Schur complement formula guarantees that dividing by the previous pivot still yields an integer.

Complexity and Numerical Behavior

  • Time complexity: $O(n^3)$ domain operations — the same as classical Gaussian elimination
  • Size of intermediate values: for integer matrices, the intermediate values $a_{ij}^{(k)}$ grow to $O(n \log n)$ digits — far better than the magnitude-$O(n!)$ growth of classical elimination
  • No rounding errors: over the integers and polynomials, all operations are exact, with no floating-point approximation

Gaussian Elimination and Row Echelon Form

Gaussian elimination transforms a matrix into row echelon form (REF):

$$ \begin{pmatrix} \boxed{a_{11}} & a_{12} & a_{13} & a_{14} \ 0 & \boxed{a_{22}} & a_{23} & a_{24} \ 0 & 0 & 0 & \boxed{a_{34}} \ 0 & 0 & 0 & 0 \end{pmatrix} $$

The boxed elements are the pivots. Row echelon form satisfies:

  1. All nonzero rows are above all zero rows
  2. The column of each pivot is all zeros below it
  3. Each pivot lies strictly to the right of the pivots in the rows above it

Continuing the elimination yields the reduced row echelon form (RREF) — every pivot is $1$ and all remaining elements in pivot columns are $0$.

Pivot Selection

A Euclidean domain need not have a notion of "absolute value" (for instance $\mathbb{F}[x]$), so oCAS's elimination does not use the partial pivoting of numerical linear algebra. Both row_echelon and determinant select the first nonzero element in the current column as the pivot; if the column is entirely zero, it is skipped (no row swap). Intermediate value growth is controlled by content stripping (see below) — after elimination, the GCD of the pivot row is immediately factored out and the whole row is divided by it.

Fraction-Free Back-Substitution

After elimination, back-substitution is needed to solve linear systems. oCAS's back-substitution is also fraction-free: starting from the last row and working upward, GCD scaling is used to avoid introducing fractions.

For row $i$ in row echelon form (pivot in column $j$) and row $k < i$ (with a nonzero element $a_{kj}$ in column $j$):

$$ g = \gcd(a_{ij}, a_{kj}), \quad s_p = \frac{a_{kj}}{g}, \quad s_r = \frac{a_{ij}}{g} $$

Update row $k$: for every $l > j$:

$$ a_{kl} \leftarrow s_r \cdot a_{kl} - s_p \cdot a_{il} $$

$$ a_{kj} \leftarrow 0 $$

This is fully consistent with the fraction-free nature of Bareiss elimination.

Content Stripping

During elimination, the coefficients of a row may share a common factor. At each step after processing a pivot, oCAS performs content stripping: it computes the GCD of the pivot row from the pivot column to the last column, then divides the entire row by that GCD.

$$ g = \gcd(a_{ij}, a_{i,j+1}, \ldots, a_{i,n-1}) $$

$$ a_{il} \leftarrow \frac{a_{il}}{g}, \quad l = j, j+1, \ldots, n-1 $$

This optimization is especially critical for polynomial rings — otherwise the degrees of the intermediate polynomials would grow exponentially.

The Inverse of a Matrix

The inverse $A^{-1}$ of a square matrix $A$ satisfies:

$$ AA^{-1} = A^{-1}A = I $$

where $I$ is the identity matrix. The inverse exists if and only if $\det(A) \neq 0$ ($A$ is nonsingular).

Computation method: transform $[A \mid I]$ by row operations into $[I \mid A^{-1}]$. In oCAS, inverse() obtains the $j$-th column of $A^{-1}$ by solving $Ax_j = e_j$ for each column $e_j$ of the identity matrix.


Implementation in oCAS

The Matrix<D: EuclideanDomain> Structure

The oCAS matrix is generic — it accepts any EuclideanDomain as the coefficient domain:

#![allow(unused)]
fn main() {
use ocas_domain::{EuclideanDomain, IntegerDomain, Integer};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let a = Matrix::new(2, 2, vec![
    Integer::from(2), Integer::from(3),
    Integer::from(5), Integer::from(7),
], d);
}

Internally it is stored as a row-major Vec<D::Element> together with the number of rows, the number of columns, and the coefficient domain value (domain: D is stored by value, not by reference). This way all arithmetic operations go through the domain's trait methods (add, mul, sub, div, gcd) and never depend on the concrete type.

Why Bareiss and Not Laplace Expansion

MethodTime complexitySpace complexityFraction explosionDomain compatibility
Laplace expansion$O(n!)$$O(n^2)$ (recursion depth $O(n)$)NoneAny commutative ring (infeasible in practice)
Classical Gaussian elimination$O(n^3)$$O(n^2)$SevereFields only
Bareiss$O(n^3)$$O(n^2)$NoneEuclidean domains
  • The $O(n!)$ complexity of Laplace expansion makes it completely unusable for $n > 10$
  • Classical elimination requires fraction arithmetic and does not work over $\mathbb{Z}$ or $\mathbb{F}[x]$
  • Bareiss's algorithm combines the best of both: $O(n^3)$ efficiency + fraction-free + support for arbitrary Euclidean domains

This is the fundamental reason oCAS chooses Bareiss — it is a general-purpose symbolic computation system, whose matrix elements may be integers, polynomials, finite-field elements, or even algebraic number field elements.

Row Echelon Form: row_echelon

#![allow(unused)]
fn main() {
// fraction-free Gaussian elimination, returns the rank
pub fn row_echelon(&mut self, max_col: usize) -> usize
}

row_echelon performs fraction-free elimination on the first max_col columns and returns the rank of the matrix. Algorithm steps:

  1. For each column $j$, select the first nonzero element in the current column as the pivot (skip the column if it is entirely zero)
  2. Content stripping: compute the GCD of the pivot row and divide the whole row by it
  3. Elimination: for each row below the pivot, eliminate column $j$ by GCD-scaled subtraction ($a_{ij}$ is the pivot and $a_{kj}$ the entry being eliminated; $s_r$ and $s_p$ must be computed before zeroing):

$$ g = \gcd(a_{ij}, a_{kj}), \quad s_r = \frac{a_{ij}}{g}, \quad s_p = \frac{a_{kj}}{g} $$

$$ a_{kj} \leftarrow 0, \qquad a_{kl} \leftarrow s_r \cdot a_{kl} - s_p \cdot a_{il} \quad (l > j) $$

This guarantees that all intermediate values stay inside the domain, with no fractions produced.

Determinant: determinant

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;
let a = Matrix::from_rows(vec![
    vec![Integer::from(1), Integer::from(2)],
    vec![Integer::from(3), Integer::from(4)],
], d);
assert_eq!(a.determinant().unwrap(), Integer::from(-2));
}

determinant implements Bareiss's algorithm directly:

  1. Initialization: copy the data into a working array, $p_0 = 1$, sign flag $\epsilon = 1$
  2. For $k = 0, \ldots, n-2$:
    • If the pivot is zero, find a nonzero row below it and swap (flip $\epsilon$)
    • Update: $a_{ij}^{(k+1)} = (a_{ij}^{(k)} \cdot a_{kk}^{(k)} - a_{ik}^{(k)} \cdot a_{kj}^{(k)}) / p_k$
    • Update the divisor: $p_{k+1} = a_{kk}^{(k)}$
  3. Return $\epsilon \cdot a_{n-1,n-1}^{(n-1)}$

Special cases: $n = 0$ returns $1$ (the determinant of the empty matrix), and $n = 1$ returns the single element directly.

Solving Linear Systems: solve

#![allow(unused)]
fn main() {
let a = Matrix::from_rows(vec![
    vec![Integer::from(1), Integer::from(1)],
    vec![Integer::from(1), Integer::from(-1)],
], d);
let b = vec![Integer::from(3), Integer::from(-1)];
let x = a.solve(&b).unwrap();
assert_eq!(x, vec![Integer::from(1), Integer::from(2)]);
}

The complete flow of solve:

  1. Augmentation: append $b$ to form $[A \mid b]$
  2. Elimination: row_echelon(nvars) to reach row echelon form
  3. Consistency check: if there is a nonzero $b$ component after the pivot rows, the system is inconsistent and MatrixError::Inconsistent is returned
  4. Back-substitution: fraction-free back_substitution(nvars)
  5. Rank check: if the rank $< n$, the system is underdetermined and MatrixError::Underdetermined is returned
  6. Final division: divide each solution component by its corresponding pivot

Error Types

ErrorMeaning
ShapeMismatch$A$ is not square, or the length of $b$ is not equal to the number of rows
RightHandSideIsNotVector$b$ is not a column vector
InconsistentThe system has no solution (rank of the augmented matrix > rank of the coefficient matrix)
Underdetermined { rank }The system has infinitely many solutions (rank < number of unknowns)
ResultNotInDomainThe solution is not in the domain (an integer matrix may produce rational solutions)

Rank and Inverse

#![allow(unused)]
fn main() {
// rank: the number of pivots in row echelon form
let rank = a.rank();

// inverse: solve Ax_j = e_j column by column
let inv = a.inverse()?;
}

rank clones the matrix, calls row_echelon, and returns the number of pivots.

inverse solves a linear system for each column of the identity matrix to obtain the columns of the inverse. This requires the matrix to be nonsingular and the solutions to lie in the domain — for example, the inverse of an integer matrix is generally not in $\mathbb{Z}$ unless $\det(A) = \pm 1$ (a unimodular matrix):

#![allow(unused)]
fn main() {
// unimodular matrix: det = 1, the integer inverse exists
let a = Matrix::from_rows(vec![
    vec![Integer::from(1), Integer::from(2)],
    vec![Integer::from(0), Integer::from(1)],
], d);
let inv = a.inverse().unwrap();
assert_eq!(inv[(0, 0)], Integer::from(1));
assert_eq!(inv[(0, 1)], Integer::from(-2));
assert_eq!(inv[(1, 0)], Integer::from(0));
assert_eq!(inv[(1, 1)], Integer::from(1));
}

Matrix Multiplication and Transpose

#![allow(unused)]
fn main() {
// multiplication: C = A · B
let c = a.matmul(&b)?;

// transpose
let at = a.transpose();

// trace (sum of the diagonal elements of a square matrix)
let tr = a.trace()?;
}

matmul uses the standard triple-loop $O(n^3)$ algorithm; all operations go through the domain's trait methods. transpose is implemented by rearranging the data. trace sums the diagonal elements of a square matrix.

Complete Example: Solving an Integer Linear System

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::matrix::Matrix;

let d = IntegerDomain;

// System of equations:
//   2x + 3y = 8
//   5x + 7y = 19
let a = Matrix::from_rows(vec![
    vec![Integer::from(2), Integer::from(3)],
    vec![Integer::from(5), Integer::from(7)],
], d);
let b = vec![Integer::from(8), Integer::from(19)];

let x = a.solve(&b).unwrap();
// x = [1, 2]
assert_eq!(x[0], Integer::from(1));
assert_eq!(x[1], Integer::from(2));

// Verify: Ax = b
let x_matrix = Matrix::new(2, 1, x, d);
let result = a.matmul(&x_matrix).unwrap();
assert_eq!(result[(0, 0)], Integer::from(8));
assert_eq!(result[(1, 0)], Integer::from(19));

// determinant: 2·7 - 3·5 = -1
assert_eq!(a.determinant().unwrap(), Integer::from(-1));
}

All intermediate values during the elimination remain integers — Bareiss's algorithm guarantees this.


Advanced Topics

Generalizations of Bareiss's Algorithm

Bareiss's algorithm can be generalized to compute determinants of submatrices (all $k \times k$ minors), which is useful for computing Smith normal forms and Hermite normal forms.

For polynomial matrices, Bareiss effectively controls coefficient growth through GCD scaling, making determinant computation of polynomial matrices feasible in practice.

Relation to LU Decomposition

Bareiss's algorithm can be viewed as a fraction-free variant of LU decomposition. Classical LU decomposition factors the matrix as $A = LU$, where $L$ is lower triangular and $U$ is upper triangular. Bareiss produces a similar decomposition, but all intermediate values stay within the domain.

$\det(A) = \det(L) \cdot \det(U) = \prod_{i} l_{ii} \cdot \prod_{i} u_{ii}$.

Matrices in Symbolic Computation

In symbolic computation, matrix elements may be polynomials or rational functions. In this setting the GCD scaling of Bareiss's algorithm is especially critical — it keeps the degree growth of polynomial coefficients at $O(n)$ instead of exponential.

The F4 algorithm of oCAS's Gröbner basis machinery makes heavy internal use of matrix row echelonization — reducing the elimination problem for multivariate polynomials to a matrix problem over the coefficient domain, then processing it efficiently with Bareiss or its $\mathbb{Z}_p$ fast path (lazy modular arithmetic).


References

  • Axler, S. Linear Algebra Done Right, 3rd ed. Springer, 2015. Chapters 4–6: determinants, eigenvalues, and inner product spaces.

  • Bareiss, E. H. "Sylvester's Identity and Multistep Integer-Preserving Gaussian Elimination." Mathematics of Computation, 22(103):565–578, 1968. The original paper on Bareiss's algorithm.

  • Cox, D., Little, J., O'Shea, D. Ideals, Varieties, and Algorithms, 4th ed. Springer, 2015. Linear algebra in polynomial matrices and Gröbner bases.

  • Geddes, K. O., Czapor, S. R., Labahn, G. Algorithms for Computer Algebra. Kluwer, 1992. Chapter 5: symbolic elimination algorithms on matrices.

Advanced: Polynomial GCD and Factorization

This chapter systematically explains the core algorithms for computing polynomial greatest common divisors (GCDs) and factorization. These algorithms are the foundational building blocks of a computer algebra system — Gröbner basis computation, rational function simplification, and the partial fraction decomposition used in ODE solving all depend on them. We start from the classical Euclidean algorithm, gradually introduce modular methods, Hensel lifting, and multivariate extensions, and finally cover factorization over finite fields and algebraic number fields.

Prerequisites

Before reading this chapter, we recommend studying:

Review of the Euclidean Algorithm

Let $\mathbb{F}$ be a field and $f, g \in \mathbb{F}[x]$ with $g \ne 0$. The Euclidean algorithm computes the GCD by repeated division with remainder:

$$f = q_1 g + r_1, \quad g = q_2 r_1 + r_2, \quad r_1 = q_3 r_2 + r_3, \quad \ldots, \quad r_{k-2} = q_k r_{k-1} + r_k$$

The last nonzero remainder $r_k$ is $\gcd(f, g)$ (up to a constant factor).

In $\mathbb{Z}[x]$, the coefficients are not field elements, so division with remainder may not be possible. We need pseudo-division:

$$\text{lc}(g)^{d+1} \cdot f = q \cdot g + r, \quad d = \deg(f) - \deg(g)$$

where $\text{lc}(g)$ is the leading coefficient of $g$. This guarantees that the quotient and remainder lie in $\mathbb{Z}[x]$, at the cost of possible coefficient growth in the remainders.

Pseudo-Remainder Sequences (PRS)

Applying the Euclidean algorithm directly to $\mathbb{Z}[x]$ with pseudo-division leads to coefficient growth: at degrees above about 16, the coefficients of intermediate results can reach hundreds of digits. To control this growth, the literature proposes several pseudo-remainder sequences:

  • Primitive PRS: at each step, take the primitive part of the pseudo-remainder (divide by the content of the coefficients), keeping the intermediate remainders primitive and avoiding needless coefficient growth
  • Subresultant PRS: carefully chosen scaling factors keep the intermediate coefficients at exactly the theoretical size of the subresultant determinants, avoiding the excessive growth of the primitive PRS

In oCAS, DenseUnivariatePolynomial::gcd uses the Euclidean algorithm with pseudo-remainders (ocas-poly/src/gcd.rs), returning a primitive GCD. Since pseudo-remainders explode at higher degrees (in practice roughly $\deg > 16$), gcd_modular_z (Brown's modular GCD, ocas-poly/src/gcd/modular.rs) should be used instead there; the two are chosen explicitly by the caller — there is no automatic degree-based dispatch.

Resultant

The resultant $\text{Res}(f, g)$ of two polynomials $f, g \in \mathbb{F}[x]$ is a scalar satisfying:

$$\text{Res}(f, g) = 0 \iff \gcd(f, g) \ne 1$$

The resultant can be computed as the determinant of the Sylvester matrix, or more efficiently via a PRS (Brown's PRS algorithm). In oCAS, the resultant() method is implemented with the Brown PRS algorithm.

Basic Concepts

Definition of GCD

Let $D$ be a unique factorization domain (UFD) and $f, g \in D[x]$. The greatest common divisor $\gcd(f, g)$ of $f$ and $g$ is the monic polynomial $d$ satisfying:

  1. $d \mid f$ and $d \mid g$ (common divisor)
  2. for any common divisor $c$, we have $c \mid d$ (maximality)

In $\mathbb{F}[x]$ ($\mathbb{F}$ a field), the GCD is always monic (multiply by the inverse of the leading coefficient). In $\mathbb{Z}[x]$, the GCD is usually taken to be primitive, i.e. with coefficients whose GCD is 1.

Content and Primitive Part

The content of a polynomial $f = \sum a_i x^i \in \mathbb{Z}[x]$ is defined as the GCD of its coefficients:

$$\text{cont}(f) = \gcd(a_0, a_1, \ldots, a_n)$$

The primitive part is defined as:

$$\text{pp}(f) = f / \text{cont}(f)$$

Gauss's lemma: the product of two primitive polynomials is again primitive. It follows that:

$$\gcd(f, g) = \gcd(\text{cont}(f),, \text{cont}(g)) \cdot \gcd(\text{pp}(f),, \text{pp}(g))$$

This allows the content (an integer GCD) and the primitive part (a polynomial GCD) to be computed separately.

In oCAS:

#![allow(unused)]
fn main() {
use ocas_poly::DenseUnivariatePolynomial;
use ocas_domain::IntegerDomain;

let f = DenseUnivariatePolynomial::from_coeffs(
    IntegerDomain,
    vec![Integer::from(6), Integer::from(4), Integer::from(2)],
);
assert_eq!(f.content(), Integer::from(2));       // cont(f) = gcd(6,4,2) = 2
assert_eq!(f.primitive_part().coeffs(),           // pp(f) = 3 + 2x + x²
    &[Integer::from(3), Integer::from(2), Integer::from(1)]);
}

Core Theory

Square-free Factorization

Motivation

The first step of complete factorization is to decompose the polynomial into powers of square-free factors:

$$f = c \cdot g_1^1 \cdot g_2^2 \cdot g_3^3 \cdots$$

where the $g_i$ are pairwise coprime and square-free ($\gcd(g_i, g_i') = 1$). This greatly simplifies the subsequent irreducible factorization — each $g_i$ only needs to be factored once.

Yun's Algorithm

For fields of characteristic 0 (such as $\mathbb{Q}$ or $\mathbb{Z}$), Yun's algorithm is based on the observation that if $f = g_1 g_2^2 g_3^3 \cdots$, then

$$\gcd(f, f') = g_2 g_3^2 g_4^3 \cdots$$

Algorithm steps:

Input: f ∈ ℤ[x] (already made primitive)
Output: [(g₁, 1), (g₂, 2), …] such that f = ∏ gₖᵏ

1. f ← pp(f),  f' ← derivative(f)
2. g ← gcd(f, f')
3. w ← f / g
4. k ← 1
5. while w ≠ 1:
6.     h ← gcd(w, g)
7.     z ← w / h          // z = gₖ (square-free factor of multiplicity k)
8.     if z ≠ 1: output (z, k)
9.     g ← g / h
10.    w ← h
11.    k ← k + 1

Key property: the GCD computation at each iteration exactly removes the factors of higher multiplicity, so $z = w/h$ contains precisely the factors of multiplicity $k$.

Complexity: $O(n^2)$ coefficient operations ($n = \deg f$), with the GCD computations as the bottleneck.

The Special Case over Finite Fields

In $\mathbb{F}_p[x]$, when $p \mid \deg f$, the formal derivative $f'$ may vanish (for example $f(x) = x^p - 1$ has $f' = 0$ over $\mathbb{F}_p$). In that case $f$ is a $p$-th power of some polynomial: $f(x) = g(x^p) = g(x)^p$. The Musser/Bernardin algorithm detects this situation, takes the $p$-th root and recurses, multiplying the multiplicities by $p$.

In oCAS, DenseUnivariatePolynomial::square_free_factorization() implements Yun's algorithm (ocas-poly/src/factor/mod.rs), and square_free_factorization_ff() implements the finite-field version (ocas-poly/src/factor/finite_field.rs).

Modular GCD (Brown 1971)

The Problem

Computing GCDs directly over $\mathbb{Z}[x]$ with pseudo-division leads to exponential coefficient growth. Brown's modular algorithm "projects" the problem onto several $\mathbb{F}_p[x]$, exploits the efficient GCD over fields, and reconstructs the answer with CRT.

Core Idea

Let $a, b \in \mathbb{Z}[x]$ and $g = \gcd(a, b)$ (primitive). The key observation:

$$\text{lc}(g) \mid \gcd(\text{lc}(a), \text{lc}(b)) \equiv \gamma$$

Therefore $\gamma^{-1} \cdot \text{lc}(g) \mid 1$, i.e. the monic GCD modulo $p$ multiplied by $\gamma$ gives a scaled version of the GCD over $\mathbb{Z}[x]$.

Algorithm Steps

Input: a, b ∈ ℤ[x] (nonzero)
Output: gcd(a, b) ∈ ℤ[x] (primitive)

1. a_p ← pp(a),  b_p ← pp(b)
2. γ ← gcd(lc(a_p), lc(b_p))    // a multiple of the leading coefficient of the GCD
3. images ← [],  best_deg ← ∞
4. for p in primes_from(2³⁰):    // start from primes greater than 2³⁰
5.     if p | γ: continue
6.     field ← FiniteField(p)
7.     g_p ← monic(gcd(a_p mod p, b_p mod p))
8.     g_scaled ← g_p · (γ mod p) · (lc(g_p)⁻¹ mod p)
9.     
10.    if deg(g_scaled) > best_deg: continue    // unlucky prime, discard
11.    if deg(g_scaled) < best_deg:             // a smaller GCD found
12.        best_deg ← deg(g_scaled)
13.        images ← [(p, g_scaled)]
14.    else: images.append((p, g_scaled))
15.    
16.    // CRT reconstruction + trial division verification
17.    candidate ← primitive_part(CRT_reconstruct(images))
18.    if candidate divides a_p AND candidate divides b_p:
19.        return candidate
20.    
21.    if the number of primes tried > MAX_PRIMES: break
22.
23. return fallback_pseudo_remainder_gcd(a, b)  // extreme fallback

Key Details

Prime selection: primes start above $2^{30}$. Each prime contributes roughly 30 bits of precision to the CRT reconstruction, so a polynomial whose coefficients have bit-length $L$ typically needs only about $L/30$ primes (in practice at most a few dozen).

Landau-$\gamma$ scaling: the monic modular GCD $g_p$ must be multiplied by $\gamma = \gcd(\text{lc}(a), \text{lc}(b))$ to align with the GCD over $\mathbb{Z}[x]$. Specifically, $\text{lc}(g_p)^{-1} \cdot \gamma \bmod p$ is the scaling factor.

CRT symmetric representatives: reconstruction uses the symmetric residue range $(-p/2, p/2]$ rather than $[0, p)$, keeping the absolute values of coefficients minimal.

Unlucky prime detection: if the degree of the modular GCD for some prime $p$ is higher than the current best, that prime is "unlucky" ($p$ divides the denominator of some coefficient of the true GCD) and is simply discarded. If the degree is lower, all previous primes were unlucky, so the images are cleared and computation restarts.

Trial division verification: the CRT-reconstructed candidate $c$ must satisfy $c \mid a_p$ and $c \mid b_p$ (exact division); otherwise more primes are collected.

Safe fallback: after at most 10000 primes (in practice rarely more than a few dozen are needed), the algorithm falls back to a pseudo-remainder GCD to guarantee termination.

In oCAS, gcd_modular_z() is implemented in ocas-poly/src/gcd/modular.rs:

#![allow(unused)]
fn main() {
use ocas_poly::gcd::modular::gcd_modular_z;

let a = DenseUnivariatePolynomial::from_coeffs(d, vec![i(-1), i(0), i(1)]); // x² - 1
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![i(1), i(2), i(1)]);  // x² + 2x + 1
let g = gcd_modular_z(&a, &b);
assert_eq!(g.coeffs(), &[i(1), i(1)]);  // x + 1
}

Multivariate GCD (Brown Evaluation–Interpolation)

The Problem

Given $f, g \in \mathbb{Z}[x_1, \ldots, x_n]$, compute $\gcd(f, g)$. The challenges for multivariate GCD are: (1) multivariate polynomials have no canonical "division with remainder"; (2) the coefficient structure is more complex.

Brown's Evaluation–Interpolation Strategy (Bivariate Case)

Let $f, g \in \mathbb{Z}[x, y]$, viewed as polynomials in the main variable $x$ with coefficients in $\mathbb{Z}[y]$.

Steps:

  1. Evaluate: choose $\alpha \in \mathbb{Z}$ and compute $f(x, \alpha)$ and $g(x, \alpha)$ (univariate polynomials)
  2. Univariate GCD: $d_\alpha(x) = \gcd(f(x, \alpha), g(x, \alpha))$
  3. Verify: check that the degree of $d_\alpha(x)$ equals the degree of the true GCD (take the minimal consistent degree over several $\alpha$)
  4. Lagrange interpolation: for sufficiently many $\alpha_1, \ldots, \alpha_k$, compute the $d_{\alpha_i}(x)$ and treat each $x$-coefficient as a polynomial in $y$, recovered by Lagrange interpolation to obtain $d(x, y)$

Key condition: $\alpha$ must be "lucky" — i.e. the degree of $\gcd(f(x, \alpha), g(x, \alpha))$ equals the degree of the true GCD. If $\alpha$ is unlucky (for example it makes a leading coefficient vanish), it must be retried.

Recursion for the Multivariate Case

An $n$-variable GCD reduces recursively by one variable:

  1. Choose an evaluation point $\alpha$ for $x_n$; compute $f|{x_n = \alpha}$ and $g|{x_n = \alpha}$
  2. Recursively compute the GCD in $n-1$ variables
  3. Interpolate to recover the dependence on $x_n$

Embedding $\mathbb{Z}$ into $\mathbb{Q}$

When computing GCDs over $\mathbb{Z}[x_1, \ldots, x_n]$, handling the content is crucial:

  1. First extract the content with respect to the main variable $x_0$ (content in $x_0$), i.e. the GCD of all $x_0^k$ coefficients
  2. Compute the multivariate GCD of the primitive parts
  3. The final GCD is $\gcd(\text{cont}_x(f), \text{cont}_x(g)) \cdot \gcd(\text{pp}_x(f), \text{pp}_x(g))$

Hensel Lifting

Core Idea

Hensel lifting is the key technique for "lifting" a factorization modulo $p$ to one modulo $p^k$ (or to $\mathbb{Z}$). Let $f \in \mathbb{Z}[x]$ be monic with

$$f \equiv g_0 \cdot h_0 \pmod{p}, \quad \gcd(g_0, h_0) = 1 \pmod{p}$$

The goal is to find $g, h \in \mathbb{Z}[x]$ such that $f = g \cdot h$, with $g \equiv g_0 \pmod{p}$ and $h \equiv h_0 \pmod{p}$.

Linear Hensel Lifting (Two-Factor Case)

Let $s, t$ be the Bézout coefficients over $\mathbb{F}_p[x]$: $s \cdot g_0 + t \cdot h_0 = 1 \pmod{p}$.

Iteration steps (from $m = p$ until $m > B$):

1. e ← f - g·h                    // the error
2. if e = 0: return (g, h)        // exact factorization
3. ē ← (e / m) mod p              // the "derivative" of the error
4. Δg ← (t · ē) mod g₀           // correction to g (deg < deg g₀)
5. Δh ← (ē - Δg · h₀) / g₀      // correction to h (exact division)
6. g ← g + Δg · m
7. h ← h + Δh · m
8. m ← m · p

Each iteration lifts the precision from $\bmod m$ to $\bmod m \cdot p$.

The Mignotte Bound

When should lifting stop? We need an upper bound on the size of the factor coefficients. The Landau–Mignotte bound states:

$$|g|_\infty \le 2^n |f|_2$$

where $n = \deg f$ and $|f|_2 = \sqrt{\sum a_i^2}$ is the 2-norm. It therefore suffices to lift until $p^k > 2 \cdot 2^n |f|_2$.

Multi-Factor Hensel Lifting

For $r > 2$ factors, a factor-by-factor peeling strategy is used:

Input: f, [g₁, g₂, …, gᵣ] mod p
Output: [G₁, G₂, …, Gᵣ] such that f = ∏ Gᵢ

1. for i = 1, …, r-1:
2.     h₀ ← g_{i+1} · g_{i+2} · … · gᵣ  (the product of the remaining factors)
3.     (Gᵢ, H) ← hensel_lift_pair(f_current, gᵢ, h₀, p, bound)
4.     f_current ← H
5. Gᵣ ← f_current

Each step is a two-factor Hensel lift.

Zassenhaus Factor Recombination

Hensel lifting produces $r$ factors $\tilde{g}_1, \ldots, \tilde{g}_r$ modulo $p^k$. The true factors over $\mathbb{Z}[x]$ are products of some subset of the $\tilde{g}_i$ (reduced modulo $p^k$ into the symmetric range $(-p^k/2, p^k/2]$).

Recombination strategy:

1. remaining ← [g̃₁, …, g̃ᵣ]
2. result ← []
3. for size = 1, 2, …, r:
4.     for each subset S ⊂ remaining of size |S|:
5.         candidate ← primitive_part(lc(rest) · ∏_{i∈S} g̃ᵢ)
6.         reduce candidate to symmetric range mod p^k
7.         if candidate divides f:
8.             result.append(candidate)
9.             remaining ← remaining \ S
10.            rest ← f / (∏ result)
11.            break  // restart from size=1
12. return result

Key optimization: multiplying by the leading coefficient of the current cofactor before taking the primitive part is Zassenhaus's classical trick — a true factor $h$ satisfies $\text{lc}(\text{rest}) \cdot \prod S = c \cdot h$.

Complexity: worst case $2^r$ subsets ($r$ = the number of mod-$p$ factors), but in practice the number of factors is usually small.

In oCAS, Hensel lifting and Zassenhaus recombination are implemented in ocas-poly/src/factor/hensel.rs:

#![allow(unused)]
fn main() {
// The complete ℤ[x] factorization pipeline:
// 1. Square-free factorization
// 2. For each square-free component:
//    a. Choose a prime p (not dividing the leading coefficient, and f mod p square-free)
//    b. Factor over 𝔽_p (Cantor-Zassenhaus)
//    c. Hensel lift to ℤ
//    d. Zassenhaus recombination
// Entry point: DenseUnivariatePolynomial::factor()
}

The Berlekamp Algorithm

Applicability

Factoring square-free polynomials over the finite field $\mathbb{F}_p$ with small prime $p$. When $p$ is small ($p \le 1000$), the matrix method is more efficient than the randomized Cantor–Zassenhaus algorithm.

Theoretical Foundation

Let $f \in \mathbb{F}_p[x]$ be a square-free polynomial of degree $n$. Define the Frobenius matrix $Q \in \mathbb{F}_p^{n \times n}$:

$$Q_{ij} = [x^j] \text{ coefficient in } x^{ip} \bmod f$$

i.e. row $i$ of $Q$ is the coefficient vector of $x^{ip} \bmod f$.

Key theorem: $v \in \mathbb{F}_p^n$ satisfies $Q^T v = v$ (i.e. $v$ lies in the null space of $Q^T - I$) if and only if the corresponding polynomial $v(x) = \sum v_i x^i$ satisfies

$$v(x)^p \equiv v(x) \pmod{f}$$

This means $v(x)$ takes values in $\mathbb{F}_p$, and

$$f \mid \prod_{a \in \mathbb{F}_p} (v(x) - a)$$

Therefore $\gcd(f, v(x) - a)$ gives a nontrivial factor of $f$ for some $a \in \mathbb{F}_p$.

Algorithm Steps

Input: monic square-free f ∈ 𝔽_p[x], deg f = n
Output: the list of irreducible factors of f

1. Construct the Frobenius matrix Q
2. Compute a basis {v₁, v₂, …, vᵣ} of the null space of Q^T - I
3. factors ← [f]
4. for each vⱼ (r ≥ 1):
5.     new_factors ← []
6.     for each factor g in factors:
7.         if deg(g) ≤ 1:
8.             new_factors.append(g); continue
9.         for a = 0, 1, …, p-1:
10.            d ← gcd(g, vⱼ - a)
11.            if 0 < deg(d) < deg(g):
12.                new_factors.append(d)
13.                g ← g / d
14.        new_factors.append(g)
15.    factors ← new_factors
16. return factors

Null space dimension: if $f$ has $r$ irreducible factors, the null space has dimension $r$. Hence only $r - 1$ nontrivial null space vectors are needed for a complete split.

Complexity: $O(n^3)$ (matrix elimination) + $O(r \cdot n^2 \cdot p)$ (GCD splitting). When $p$ is large, the cost of the $a$-loop becomes prohibitive and Cantor–Zassenhaus should be used instead.

In oCAS, berlekamp() is implemented in ocas-poly/src/factor/finite_field.rs. It is used automatically when $p \le 1000$.

The Cantor–Zassenhaus Algorithm

Applicability

Factoring square-free polynomials over the finite field $\mathbb{F}_p$ with large prime $p$. It proceeds in two stages: distinct-degree factorization (DDF) and equal-degree factorization (EDF).

Distinct-Degree Factorization (DDF)

Goal: decompose $f$ as $f = g_1 \cdot g_2 \cdots g_s$, where every irreducible factor of $g_d$ has degree exactly $d$.

Theoretical foundation: the elements of $\mathbb{F}_{p^d}$ are exactly the roots of $x^{p^d} - x = 0$. Hence the irreducible factors of $f$ of degree $d$ divide exactly

$$\gcd(f,, x^{p^d} - x)$$

but do not divide the earlier $x^{p^{d'}} - x$ ($d' < d$).

Algorithm:

Input: monic square-free f ∈ 𝔽_p[x]
Output: [(g₁, 1), (g₂, 2), …] where the factors of gₖ have degree k

1. current ← f,  h ← x,  degree ← 1
2. while deg(current) ≥ 2·degree:
3.     h ← h^p mod current           // Frobenius iteration: h = x^(p^degree)
4.     g ← gcd(current, h - x)
5.     if deg(g) > 0:
6.          output (monic(g), degree)
7.         current ← current / g
8.         h ← h mod current
9.     degree ← degree + 1
10. if deg(current) > 0:
11.      output (monic(current), deg(current))

Optimizing the Frobenius iteration: $x^{p^d}$ need not be computed directly (the exponent is enormous); instead iterate $h \leftarrow h^p \bmod f$ step by step, using fast modular exponentiation each time.

Complexity: $O(n^2 \log p)$ per Frobenius iteration, $O(n^3 \log p)$ in total.

Equal-Degree Factorization (EDF)

Goal: completely split $f = g_1 g_2 \cdots g_r$ (each $g_i$ irreducible of degree $d$).

Odd characteristic case ($p > 2$):

Use the fact that exactly half of the elements of $\mathbb{F}_{p^d}$ are quadratic residues. For a random polynomial $a$, compute

$$b = a^{(p^d - 1)/2} \bmod f$$

Then $b$ takes the value $1$ on each irreducible factor $g_i$ where $a$ is a quadratic residue in that factor's field, and $-1$ where it is a non-residue. Therefore

$$\gcd(f,, b - 1) = \prod_{i:, a \text{ is a quadratic residue in } g_i} g_i$$

gives a nontrivial split of $f$ (unless all $g_i$ agree, which happens with probability $2^{1-r}$).

Characteristic 2 case ($p = 2$):

The $b - 1$ trick does not apply ($1 = -1$). Instead use the trace map:

$$T(a) = a + a^2 + a^{2^2} + \cdots + a^{2^{d-1}} \bmod f$$

$T(a)$ takes values in $\mathbb{F}2$ on each $\mathbb{F}{2^d}$ (a property of the trace), and for random $a$ the values on different factors are independent. Hence $\gcd(f, T(a))$ gives a nontrivial split.

Input: f (the product of the d-degree factors from DDF)
Output: the list of irreducible factors of f

1. factors ← [f]
2. while there exists a factor with deg > d:
3.     for each factor g with deg(g) > d:
4.         choose a random polynomial a (deg < deg(g))
5.         if p = 2:
6.             b ← T(a) = Σᵢ₌₀^{d-1} a^{2^i} mod g
7.         else:
8.             b ← a^{(p^d-1)/2} mod g
9.         d₁ ← gcd(g, b - 1)   // or gcd(g, b) for char 2
10.        if 0 < deg(d₁) < deg(g):
11.            replace g by d₁ and g/d₁ in factors
12. return factors

In oCAS, cantor_zassenhaus() is implemented in ocas-poly/src/factor/finite_field.rs; the top-level entry factor_over_finite_field() automatically chooses Berlekamp ($p \le 1000$) or Cantor–Zassenhaus according to the size of $p$.

Wang's EEZ Multivariate Factorization

The Problem

Given $f \in \mathbb{Z}[x_1, \ldots, x_n]$ (or $\mathbb{F}_p[x_1, \ldots, x_n]$), factor it into irreducible factors.

Strategy Overview

Wang's (1978) EEZ (Evaluation, Exact division, Zassenhaus) algorithm generalizes univariate Hensel lifting to the multivariate case:

  1. Evaluate: choose evaluation points for the auxiliary variables to obtain a univariate image $f(x_1, \alpha_2, \ldots, \alpha_n)$
  2. Univariate factorization: factor the univariate image over $\mathbb{Z}$ (or $\mathbb{F}_p$)
  3. Variable-by-variable Hensel lifting: lift back to the multivariate factorization one variable at a time
  4. Zassenhaus recombination: combine the lifted factors

Wang's Leading-Coefficient Preprocessing

One difficulty of multivariate factorization is the nonconstant leading coefficient. Let $\ell(x_1, \ldots, x_n)$ be the leading coefficient of $f$ in the main variable $x_0$. If $\ell$ is not constant, the leading coefficients $\ell_i$ of the factors $f_i$ need not be constant either, and $\prod \ell_i = \ell$.

Wang's greedy assignment:

  1. Factor $\ell$ into powers of irreducible factors: $\ell = \prod g_j^{e_j}$
  2. At the evaluation point $\alpha = (\alpha_2, \ldots, \alpha_n)$, the image of $\ell$ splits into the images of the univariate factors
  3. Assign the nontrivial factors $g_j$ of $\ell$ to the corresponding $f_i$ so that $g_j(\alpha) = \text{lc}(u_i)$ ($u_i$ the univariate image factor)
  4. Ensure consistency of the assignment via pairwise coprimality conditions ($\alpha_j = |g_j(\alpha)| > 1$)

Variable-by-Variable Hensel Lifting

Let $f$ have $n$ variables and let the univariate image factors be $u_1, \ldots, u_r$.

Lifting the variable $x_k$ ($k = 1, 2, \ldots, n-1$, one variable at a time in index order):

  1. Evaluate the current factors $F_i^{(k-1)}$ (in the variables $x_1, \ldots, x_{k-1}$) at $x_k = \alpha_k$ to obtain the $u_i$
  2. Compute Bézout coefficients $b_i$ such that $\sum b_i \prod_{j \ne i} u_j = 1$
  3. For $t = 1, 2, \ldots$, successively solve the multivariate Diophantine equation:

$$\sum_i \sigma_i \cdot \prod_{j \ne i} F_j = e_t$$

where $e_t$ is the $t$-th term of the Taylor expansion of the current error $f - \prod F_i$

  1. Correct $F_i \leftarrow F_i + \sigma_i \cdot (x_k - \alpha_k)^t$

The Multivariate Diophantine Equation Solver

When lifting the variable $x_k$, the equations to solve have the form:

$$\sum_{i=1}^{r} \sigma_i \cdot \prod_{j \ne i} u_j = e \pmod{(x_k - \alpha_k)^t}$$

with $\deg_{x_0}(\sigma_i) < \deg_{x_0}(u_i)$. This is a linear system (in the coefficients of the $\sigma_i$) that can be solved recursively:

  • when $k = 1$ (univariate): the extended Euclidean algorithm
  • when $k > 1$: recurse on $k-1$ variables — evaluate at $\alpha_k$, solve the smaller system, then interpolate

p-adic Coefficient Hensel Lifting

For the nonconstant leading coefficient case over $\mathbb{Z}[x_1, \ldots, x_n]$, Wang EEZ must be followed by p-adic coefficient lifting:

  1. Reduce the coefficients of the multivariate factors modulo $p$ to obtain a skeleton over $\mathbb{F}_p$
  2. Iteratively solve the mod-$p$ Diophantine equations, lifting the coefficients step by step from $\bmod p$ to $\bmod p^k$
  3. Continue until the error vanishes or $p^k$ exceeds the coefficient bound (the Gelfond bound)

The Gelfond coefficient bound:

$$B = \left(\sqrt{\prod_v (d_v + 1) \cdot 2^{2 \sum d_v - n}} + 1\right) \cdot |f|_\infty \cdot |\text{lc}(f)|$$

where $d_v$ is the degree in the variable $v$ and $n$ is the number of variables.

Sparse Diophantine Solving

When the Diophantine correction terms have a sparse structure, dense recursive solving can be avoided. oCAS implements a skeleton interpolation strategy:

  1. Extract the skeleton from the terms of the error $e$ — the possible exponent patterns of the correction terms
  2. Evaluate at several random base points to obtain univariate Diophantine equations
  3. Interpolate the coefficients through a Vandermonde system
  4. Verify the interpolated result

This is especially effective when the factors have many variables but low degree in each.

In oCAS, the general EEZ algorithm is implemented in ocas-poly/src/factor/eez.rs, with a bivariate specialization in ocas-poly/src/factor/multivariate.rs (which currently requires the leading coefficient in the main variable to be constant):

#![allow(unused)]
fn main() {
// Multivariate factorization entry over ℤ (eez.rs):
pub fn multivariate_factor_z(f: &ZmPoly) -> Vec<(ZmPoly, usize)>

// Multivariate factorization entry over 𝔽_p (eez.rs):
pub fn multivariate_factor_fp(f: &FpMPoly) -> Vec<(FpMPoly, usize)>

// Bivariate specialization entries (multivariate.rs):
pub fn bivariate_factor_z(f: &ZMPoly, x_var: usize, y_var: usize) -> Vec<(ZMPoly, usize)>
pub fn bivariate_factor_fp(f: &FpMPoly, x_var: usize, y_var: usize) -> Vec<(FpMPoly, usize)>
}

Trager Factorization over Algebraic Number Fields

The Problem

Given $f \in K[x]$ where $K = \mathbb{Q}(\alpha)$ is an algebraic number field ($\alpha$ a root of the minimal polynomial $m(\alpha) = 0$), factor $f$ into irreducible factors in $K[x]$.

Reduction via the Norm

The core idea of Trager's algorithm is to reduce the problem from $K[x]$ to $\mathbb{Q}[x]$ via the norm:

$$N(f) = \text{Res}_\alpha(m(\alpha),, f(x, \alpha))$$

The norm $N(f) \in \mathbb{Q}[x]$ has degree $\deg_x(f) \cdot [K:\mathbb{Q}]$, and if $f$ is reducible in $K[x]$, then $N(f)$ is reducible in $\mathbb{Q}[x]$.

Computing the Norm by Evaluation–Interpolation

Constructing the Sylvester matrix directly to compute the resultant is expensive. oCAS uses evaluation–interpolation:

  1. Compute the scalar resultants $\text{Res}_\alpha(m, f(x_j, \alpha))$ at $\deg_x(f) \cdot [K:\mathbb{Q}] + 1$ rational points $x_j$
  2. Each scalar resultant is the norm of an element of $\mathbb{Q}(\alpha)$, obtained by evaluating $m$ at $[K:\mathbb{Q}]$ points and taking the product
  3. Recover $N(f) \in \mathbb{Q}[x]$ by Newton divided-difference interpolation

The Trager Shift

If $N(f)$ has repeated factors, information about the factors of $f$ is lost. The Trager shift substitutes $x \mapsto x - s\alpha$ ($s \ge 0$) to make the norm square-free:

$$N(f(x - s\alpha)) \text{ is square-free}$$

Such an $s$ always exists (only finitely many $s$ are bad); in practice $s = 0$ or a small value suffices.

Factorization Steps

Input: f ∈ K[x] (square-free)
Output: the irreducible factors of f in K[x]

1. for s = 0, 1, 2, …:
2.     g ← f(x - s·α)
3.     N ← Res_α(m, g(x, α))  via evaluation-interpolation
4.     if N is square-free: break
5. 
6. N₁, …, Nₖ ← factor_over_Q(N)   // factor over ℚ
7. for each Nᵢ:
8.     N̂ᵢ ← embed_Q_to_K(Nᵢ)
9.     hᵢ ← gcd_K(g, N̂ᵢ)          // GCD over K
10.    output ← compose_linear(hᵢ, +s·α)   // inverse shift
11. return [monic(output₁), …]

Modular GCD over Algebraic Number Fields

The $K[x]$ GCD in step 9 is computed efficiently by modular methods (gcd_anf):

  1. Choose a prime $p$ such that $m$ is irreducible over $\mathbb{F}_p$ (ensuring $\mathbb{F}_p[\alpha]/(m) \cong \text{GF}(p^d)$)
  2. Map $a, b \in K[x]$ to $\text{GF}(p^d)[x]$
  3. Compute the monic GCD over $\text{GF}(p^d)[x]$
  4. CRT combination + rational reconstruction + trial division verification

Unlucky primes (where the modular GCD degree is too large) are discarded; after at most 1000 primes the algorithm falls back to a dense Euclidean GCD.

In oCAS, Trager's algorithm is implemented in ocas-poly/src/factor/algebraic.rs:

#![allow(unused)]
fn main() {
// Factorization entry over K = ℚ(α):
impl DenseUnivariatePolynomial<AlgebraicNumberField> {
    pub fn factor(&self) -> Factors<AlgebraicNumberField>
}

// Example: x² - 2 factors over ℚ(√2) as (x - √2)(x + √2)
let field = AlgebraicNumberField::new(
    RationalDomain,
    vec![Rational::new(-2, 1), Rational::new(0, 1), Rational::new(1, 1)],
);
let f = DenseUnivariatePolynomial::from_coeffs(field.clone(), vec![
    field.from_base(Rational::new(-2, 1)),
    field.zero(),
    field.one(),
]);
let factors = f.factor();
assert_eq!(factors.len(), 2);  // (x - √2) and (x + √2)
}

Implementation in oCAS

File Map

Source fileAlgorithmDescription
ocas-poly/src/dense.rsEuclidean GCD, Karatsuba multiplicationCore data structure for univariate polynomials
ocas-poly/src/gcd/modular.rsBrown modular GCDEfficient GCD over $\mathbb{Z}[x]$
ocas-poly/src/factor/mod.rsTop-level entry, Yun square-free factorizationfactor() and square_free_factorization()
ocas-poly/src/factor/finite_field.rsBerlekamp, Cantor–Zassenhaus (DDF + EDF)Factorization over $\mathbb{F}_p[x]$
ocas-poly/src/factor/hensel.rsHensel lifting, Zassenhaus recombination, Mignotte boundThe $\mathbb{Z}[x]$ factorization pipeline
ocas-poly/src/factor/multivariate.rsBivariate factorization (Wang EEZ specialization; leading coefficient in the main variable must be constant)$\mathbb{Z}[x,y]$ and $\mathbb{F}_p[x,y]$
ocas-poly/src/factor/eez.rsGeneral multivariate EEZ Hensel lifting$\mathbb{Z}[x_1,\ldots,x_n]$ and $\mathbb{F}_p[x_1,\ldots,x_n]$
ocas-poly/src/factor/algebraic.rsTrager's algorithm (factorization over algebraic number fields)$\mathbb{Q}(\alpha)[x]$
ocas-poly/src/multivariate_gcd.rsMultivariate GCD (evaluation–interpolation)Supports multivariate factorization

Algorithm Selection Strategy

oCAS automatically selects the best algorithm according to the input:

factor(f ∈ ℤ[x]):
  1. Square-free factorization (Yun's algorithm)
  2. For each square-free component g:
     a. if deg(g) ≤ 1: return it directly
     b. choose a prime p (not dividing lc(g), and g mod p square-free)
     c. factor over 𝔽_p (Cantor–Zassenhaus; the general factor_over_finite_field entry switches to Berlekamp automatically when p ≤ 1000)
     d. if the number of mod-p factors = 1: g is irreducible, return
     e. Hensel lifting (the Mignotte bound determines the precision)
     f. Zassenhaus factor recombination
     g. non-monic case: leading coefficient transformation a^{d-1}·f(x/a)

factor(f ∈ 𝔽_p[x]):
  1. Extract the leading coefficient
  2. Square-free factorization (Musser/Bernardin, handling characteristic p)
  3. For each square-free component: Berlekamp (small p) or Cantor-Zassenhaus

factor(f ∈ ℚ(α)[x]):
  1. Yun square-free factorization (using the modular GCD gcd_anf)
  2. For each component: Trager norm + ℚ factorization + K-GCD recovery

factor(f ∈ ℤ[x₁,…,xₙ]):
  1. Extract the content (in the main variable x₀)
  2. Square-free factorization
  3. Wang leading-coefficient preprocessing
  4. Choose evaluation points, factor the univariate image
  5. EEZ variable-by-variable Hensel lifting
  6. p-adic coefficient lifting (when the leading coefficient is nonconstant)
  7. Zassenhaus recombination

Performance Characteristics

AlgorithmTime complexitySpace complexityUse case
Euclidean GCD (PRS)$O(n^2)$$O(n)$Low degree ($\lesssim 16$)
Brown modular GCD$O(n^2 \cdot k)$$O(n \cdot k)$High-degree $\mathbb{Z}[x]$, $k$ = number of primes
Hensel lifting$O(n^2 \cdot \log B)$$O(n \cdot r)$$r$ factors, $B$ = Mignotte bound
Zassenhaus recombination$O(2^r \cdot n)$$O(n \cdot r)$$r$ mod-$p$ factors
Berlekamp$O(n^3 + r \cdot n^2 p)$$O(n^2)$Small $p$
Cantor–Zassenhaus$O(n^3 \log p)$$O(n)$Large $p$
Wang EEZExponential in the number of variablesMultivariate polynomialsMultivariate factorization
Trager$O(n^3 [K:\mathbb{Q}]^2)$$O(n \cdot [K:\mathbb{Q}])$Algebraic number fields

References

  • [Gathen–Gerhard] J. von zur Gathen and J. Gerhard, Modern Computer Algebra, 3rd ed., Cambridge University Press, 2013. Chapters 14–15 (factoring), 18 (GCD).
  • [Brown] W. S. Brown, "On Euclid's Algorithm and the Computation of Polynomial Greatest Common Divisors," J. ACM, 18(4):478–504, 1971.
  • [Zassenhaus] H. Zassenhaus, "On Hensel Factorization I," J. Number Theory, 1(3):291–311, 1969.
  • [Wang] P. S. Wang, "An Improved Multivariate Polynomial Factoring Algorithm," Math. Comp., 32(144):1215–1231, 1978.
  • [Berlekamp] E. R. Berlekamp, "Factoring Polynomials over Large Finite Fields," Math. Comp., 24(111):713–735, 1970.
  • [Cantor–Zassenhaus] D. G. Cantor and H. Zassenhaus, "A New Algorithm for Factoring Polynomials over Finite Fields," Math. Comp., 36(154):587–592, 1981.
  • [Trager] B. M. Trager, "Algebraic Factoring and Rational Function Integration," Proc. SYMSAC '76, pp. 219–226, 1976.
  • [Mignotte] M. Mignotte, Mathematics for Computer Algebra, Springer, 1992.
  • [Geddes–Czapor–Labahn] K. O. Geddes, S. R. Czapor, and G. Labahn, Algorithms for Computer Algebra, Kluwer, 1992. Chapters 6 (multivariate factoring), 7 (GCD).
  • [Knuth] D. E. Knuth, The Art of Computer Programming, Vol. 2, §4.6.2, Addison-Wesley, 1997.

Advanced: Gröbner Basis Theory

Gröbner bases are the core tool of the theory of ideals in multivariate polynomial rings. This chapter starts from the basic definitions of monomial ideals and S-polynomials, systematically explains the theoretical foundations of Buchberger's algorithm, the F4 matrix method, and the F5 signature method, and describes how oCAS implements these algorithms in Rust.


Prerequisites

Before reading this chapter, we recommend familiarity with the following:

  • Polynomial algebra: the multivariate polynomial ring $\mathbb{F}[x_1, \ldots, x_n]$, monomial orders (Lex / Grlex / Grevlex), multivariate division with remainder — see Polynomial Algebra
  • Linear algebra: matrix operations, Gaussian elimination, row echelon form — see Linear Algebra
  • Finite fields: construction and basic operations of $\mathbb{F}_p$ — see Finite Fields and Modular Arithmetic
  • Ideals and quotient rings: basic notions of ideals $I \subseteq \mathbb{F}[x_1, \ldots, x_n]$ and quotient rings $R/I$

For a systematic learning path, see path A in the Mathematical Foundations Overview.


Basic Concepts

Monomial Ideals

Definition. Let $R = \mathbb{F}[x_1, \ldots, x_n]$. An ideal generated by monomials

$$M = \langle x^{\alpha^{(1)}}, x^{\alpha^{(2)}}, \ldots, x^{\alpha^{(s)}} \rangle$$

is called a monomial ideal. We have $f \in M$ if and only if the monomial of every nonzero term of $f$ is divisible by some $x^{\alpha^{(i)}}$.

Example. $M = \langle x^2, xy \rangle \subseteq \mathbb{F}[x, y]$. The terms of $f = x^2 y + x y^2$: $x^2 y$ is divisible by $x^2$, and $xy^2$ is divisible by $xy$, so $f \in M$. But the monomial $y^3$ of $g = y^3$ is divisible by neither $x^2$ nor $xy$, so $g \notin M$.

Dickson's Lemma

Lemma (Dickson). Every subset $S$ of $\mathbb{N}^n$ has a finite set $M \subseteq S$ of minimal elements such that for every $\alpha \in S$ there exists $\beta \in M$ with $\beta \leq \alpha$ (componentwise comparison).

Equivalent formulation: every monomial ideal in $\mathbb{F}[x_1, \ldots, x_n]$ is generated by finitely many monomials.

Proof sketch (induction on $n$).

  • $n = 1$: every nonempty subset of $\mathbb{N}$ has a least element; take it as $M$.
  • $n \to n+1$: for $S \subseteq \mathbb{N}^{n+1}$, consider the sections $S_a = {(\alpha_2, \ldots, \alpha_{n+1}) : (a, \alpha_2, \ldots, \alpha_{n+1}) \in S}$ for each $a \in \mathbb{N}$. By the induction hypothesis each $S_a$ has finitely many minimal elements. Further analyzing the elements "dominated by smaller first components" allows the construction of a finite minimal set for $S$.

Significance: Dickson's lemma guarantees the finite generation of monomial ideals, which directly implies the Hilbert basis theorem — every polynomial ideal is finitely generated. Gröbner basis theory builds a systematic method for "finding suitable generators" on this foundation.

S-polynomials

Definition. Let $f, g \in R$ be nonzero polynomials. Let $\gamma = \text{lcm}(\text{lm}(f), \text{lm}(g))$ — the componentwise maximum of the exponent vectors. The S-polynomial of $f$ and $g$ is

$$S(f, g) = \frac{x^\gamma}{\text{lt}(f)} \cdot f - \frac{x^\gamma}{\text{lt}(g)} \cdot g = \frac{x^\gamma}{\text{lm}(f)} \cdot f - \frac{x^\gamma}{\text{lm}(g)} \cdot g$$

The key property of the S-polynomial is that its leading term is canceled. Intuitively, $S(f,g)$ measures the "inconsistency" of $f$ and $g$ beyond their leading terms. When the leading monomials of $f$ and $g$ are coprime (i.e. $\text{lcm} = \text{lm}(f) \cdot \text{lm}(g)$), $S(f,g)$ can usually be reduced directly to zero — this is Buchberger's first optimization criterion.

Example. In $\mathbb{F}[x, y]$, take $f = x^2 - y$ and $g = xy - 1$ (Grevlex order).

$$\gamma = \text{lcm}(x^2, xy) = x^2 y$$

$$S(f, g) = \frac{x^2 y}{x^2} \cdot (x^2 - y) - \frac{x^2 y}{xy} \cdot (xy - 1) = y(x^2 - y) - x(xy - 1) = -y^2 + x$$

Definition of a Gröbner Basis

Definition. Fix a monomial order $\succ$. A generating set ${g_1, \ldots, g_t}$ of an ideal $I$ is called a Gröbner basis of $I$ with respect to $\succ$ if

$$\langle \text{lt}(g_1), \ldots, \text{lt}(g_t) \rangle = \langle \text{lt}(f) : f \in I \rangle$$

i.e. the ideal generated by the leading terms of the generators equals exactly the ideal generated by the leading terms of all polynomials in $I$ (the leading term ideal $L(I)$ of $I$).

Equivalent characterization (Buchberger's criterion). ${g_1, \ldots, g_t}$ is a Gröbner basis of $I$ if and only if for every pair $i \neq j$, the remainder of $S(g_i, g_j)$ on division by ${g_1, \ldots, g_t}$ is zero.

The core value of Gröbner bases:

  1. Membership testing: $f \in I$ if and only if the remainder of $f$ on division by the Gröbner basis is zero (uniqueness of the normal form)
  2. Equality testing of ideals: two ideals satisfy $I = J$ if and only if their (reduced) Gröbner bases coincide
  3. Elimination: a Gröbner basis under the Lex order naturally has the elimination property (generators of the elimination ideal appear in the basis)
  4. Structure of the quotient ring: a $k$-vector space basis of $R/I$ is given by the monomials not divisible by any leading monomial of the ideal (the staircase)

Reduced Gröbner bases. A Gröbner basis ${g_1, \ldots, g_t}$ is reduced if:

  1. each $g_i$ is monic ($\text{lc}(g_i) = 1$)
  2. for $i \neq j$, no nonzero term of $g_i$ is divisible by $\text{lm}(g_j)$

The reduced Gröbner basis is unique (for a fixed order and field). All of oCAS's algorithms output reduced Gröbner bases.


Core Theory

Buchberger's Algorithm

Buchberger's algorithm (1965) is the classical method for computing Gröbner bases. Its idea is simple and direct: repeatedly construct S-polynomials and reduce them until all S-polynomials reduce to zero.

Input: F = {f1, ..., fs} (generators of the ideal)
Output: G (Gröbner basis)

G ← F
P ← {(fi, fj) | i < j}  // initial set of critical pairs
while P ≠ ∅:
    take a pair (f, g) from P
    h ← NF(S(f, g), G)   // remainder of the S-polynomial on division by G
    if h ≠ 0:
        P ← P ∪ {(h, g') | g' ∈ G}  // add new critical pairs
        G ← G ∪ {h}
return G

Termination: guaranteed by Buchberger's criterion — each new element strictly enlarges the leading term ideal, and the Hilbert basis theorem guarantees the chain terminates. In practice the implementation also needs minimize (removing redundant elements) and auto_reduce (mutual reduction) to obtain the reduced basis.

Efficiency issues: the main bottlenecks of the naive Buchberger algorithm are —

  1. Number of critical pairs: $O(|G|^2)$ critical pairs, most of which are redundant
  2. One at a time: only one S-polynomial is reduced per step; no batching is exploited
  3. Repeated scanning: finding a reducer requires a linear scan of the whole basis

The following three sections introduce the methods that address these problems: Gebauer–Moeller filtering (Buchberger speedup), F4 (batching), and F5 (avoiding reductions to zero).

Buchberger's Criteria and Optimizations

Buchberger's two criteria can drastically reduce the number of critical pairs that must be processed.

First criterion (coprime leading monomials). If $\text{lm}(f_i)$ and $\text{lm}(f_j)$ are coprime (i.e. $\gcd = 1$), then

$$S(f_i, f_j) \xrightarrow{G} 0$$

i.e. this S-polynomial reduces directly to zero, and the critical pair can be removed from the set.

Proof: let $\text{lm}(f_i) = m_i$ and $\text{lm}(f_j) = m_j$ with $\gcd(m_i, m_j) = 1$. Then $\text{lcm}(m_i, m_j) = m_i m_j$ and $S(f_i, f_j) = m_j f_i - m_i f_j$. When reducing term by term, every nonzero term $c \cdot x^\alpha$ satisfies that $x^\alpha$ is divisible by either $m_i$ or $m_j$, so it can be reduced by the corresponding $f_i$ or $f_j$; the final remainder is zero. $\square$

Second criterion (chain criterion / generalization of Buchberger's criterion). Let $f_k \in G$. If $\text{lm}(f_k)$ divides $\text{lcm}(\text{lm}(f_i), \text{lm}(f_j))$, and both $\text{lcm}(\text{lm}(f_i), \text{lm}(f_k))$ and $\text{lcm}(\text{lm}(f_j), \text{lm}(f_k))$ strictly divide $\text{lcm}(\text{lm}(f_i), \text{lm}(f_j))$, then

$$S(f_i, f_j) \xrightarrow{G} 0$$

and the critical pair $(f_i, f_j)$ need not be processed.

Redundant pairs: more generally, if the $\text{lcm}$ of a critical pair $(f_i, f_j)$ is divisible by the $\text{lcm}$ of another existing critical pair, then $(f_i, f_j)$ is redundant — processing the other pair already subsumes this one.

Gebauer–Moeller Critical Pair Management

Gebauer and Moeller (1988) systematized Buchberger's criteria into an efficient framework for managing critical pairs. When a new polynomial $f_{\text{new}}$ is added to the basis $G = {f_1, \ldots, f_m}$:

  1. Generate candidate pairs: $P_{\text{new}} = {(f_{\text{new}}, f_i) \mid 1 \leq i \leq m}$
  2. First-criterion filtering: delete the pairs where $\text{lm}(f_{\text{new}})$ and $\text{lm}(f_i)$ are coprime
  3. Second-criterion filtering: delete the pairs whose $\text{lcm}$ is divided by the $\text{lcm}$ of another (still live) pair (divisibility includes equality; duplicate pairs are dropped)
  4. Redundant pair cleanup: mark old pairs whose $\text{lcm}$ is divisible by the $\text{lcm}$ of a new pair as redundant
  5. Merge: merge the filtered $P_{\text{new}}$ into the main critical pair set

Key optimization: sort the critical pairs by the size of their $\text{lcm}$ (compared with the monomial order) and process the small ones first — this makes the larger pairs more likely to be filtered out by the second criterion.

The F4 Algorithm: Batched Matrix Row Echelon

The core idea of the F4 algorithm (Faugère 1999) is: batch the reduction of many S-polynomials into a single sparse-matrix row-echelon computation.

Symbolic Preprocessing

F4 does not divide S-polynomials directly; instead it first performs symbolic preprocessing: for the selected set of critical pairs, find all the reducers needed (monomial multiples of basis elements), expand their nonzero terms, and map them to column indices.

Concrete steps:

  1. Select critical pairs: take a batch of critical pairs from the queue (sorted by $\text{lcm}$)
  2. Collect monomials: for each selected critical pair, compute the monomials appearing in the S-polynomial; for each monomial that needs reduction, find a reducer in the basis and expand it multiplied by the corresponding monomial
  3. Build the column map: order all monomials that occur and number them $0, 1, \ldots, N-1$
  4. Construct the matrix: each row corresponds to a polynomial (an S-polynomial or a multiple of a reducer), each column to a monomial, and the matrix entries are coefficients

This produces a sparse matrix $M$ in which the nonzero entries of each row correspond to the nonzero terms of a polynomial.

Row Echelon Computation

Perform Gaussian elimination (row echelonization) on the matrix $M$:

  1. Sort the rows by their first nonzero column
  2. Eliminate column by column: find a row whose first nonzero entry is in the current column to serve as the pivot, and eliminate that column in the remaining rows
  3. Among the rows after elimination, the polynomials corresponding to the nonzero rows are the new basis elements

ℤ_p native fast path. When the coefficient domain is the finite field $\mathbb{F}_p$ ($p < 2^{31}$), all matrix operations use i64 modular arithmetic — no BigInt overhead. This includes:

  • the sparse row subtraction sub_scaled_fp: merge two rows in ascending column order, skip the first column (already eliminated), compute $(row_j - c \cdot pivot_j) \bmod p$ term by term, and drop zero entries
  • the modular inverse lookup mod_inv: extended Euclid
  • the modular reduction norm_mod: keep coefficients in $[0, p)$

Extracting New Basis Elements

After elimination, every nonzero row $r$ of the matrix corresponds to a polynomial $h_r$. For $h_r$:

  1. if the leading term of $h_r$ is "new" (not in the set of leading terms collected so far), add it to the basis
  2. further simplify with tail reduction: reduce the tail terms of $h_r$ using the existing basis elements

Complexity of F4

The main cost of F4 is the row-echelon computation — for a matrix with $m$ rows and $N$ columns, Gaussian elimination requires $O(m \cdot N \cdot \text{nnz_per_row})$ field operations. In practice the matrices are sparse ($\text{nnz_per_row} \ll N$), and since Gebauer–Moeller filtering reduces the number of critical pairs, F4 is one to two orders of magnitude faster than the naive Buchberger algorithm.

The F5 Algorithm: Signature-Based Gröbner Bases

The revolutionary contribution of the F5 algorithm (Faugère 2002) is that it identifies and rejects S-polynomials that would reduce to zero before the matrix is even constructed. This is achieved by attaching a signature to every polynomial.

Signatures and Module Representations

The ideal generation problem is turned into a module element problem. Let $I = \langle f_1, \ldots, f_s \rangle$ and consider the standard basis $e_1, \ldots, e_s$ of the free module $R^s$. Every polynomial $h = \sum q_i f_i$ corresponds to a module element

$$\vec{h} = (q_1, \ldots, q_s) = \sum q_i e_i$$

The signature of $h$ is defined as the "highest" monomial multiple in the module element:

$$\text{sig}(h) = \max_\succ {x^{\alpha_i} e_i : q_i \neq 0}$$

where the comparison uses the pot (position-over-term) order: first compare the module positions (the indices $i$ of the $e_i$; a smaller index gives a smaller signature, processed earlier), then the monomials. That is, $x^\alpha e_i \succ x^\beta e_j$ if and only if $i > j$, or $i = j$ and $\alpha \succ \beta$.

Intuition: the signature records the "origin" of the polynomial $h$ — which multiple ($x^\alpha$) of which generator ($f_i$) it was derived from. The signature of a polynomial is the minimal representation of its derivation history.

The F5 Criterion

Signature criterion. If the signature of a candidate S-polynomial is a monomial multiple of some known syzygy, then this S-polynomial necessarily reduces to zero — it can be rejected directly, without entering the matrix.

Meaning of a syzygy: when a row of the matrix reduces to zero, the corresponding polynomial satisfies $h = 0$, but its module representation is $(q_1, \ldots, q_s) \neq 0$. This yields a syzygy (relation) $\sum q_i f_i = 0$. The signature $\text{sig}(h)$ of that row records this syzygy.

The SyzygySet data structure: for each module position $k$, maintain the set of leading monomials of the known syzygies. To test whether a signature $(k, t)$ is a syzygy, check whether some stored leading monomial of a $k$-position syzygy divides $t$.

Regular Sequences and Complete Avoidance of Reductions to Zero

Theorem (Faugère 2002). If the input generators $f_1, \ldots, f_s$ form a regular sequence, then the F5 signature criterion suffices to reject all reductions to zero — i.e. no row of the matrix ever reduces to zero.

Definition of a regular sequence: $f_1, \ldots, f_s$ form a regular sequence if $f_1$ is not a zero divisor and, for $i > 1$, $f_i$ is not a zero divisor in $R/\langle f_1, \ldots, f_{i-1} \rangle$.

In practice, even when the generators do not form a regular sequence, the F5 signature criterion still filters out a large number of reductions to zero. In the literature, F5 achieves order-of-magnitude speedups over F4 on hard benchmark ideals (the oCAS F5 implementation is still being optimized).

Matrix Construction in F5

F5's matrix construction is similar to F4's, but every row additionally carries signature information:

  1. Incremental processing: add the generators $f_1, f_2, \ldots, f_s$ one at a time
  2. Labeled S-polynomials: every S-polynomial enters the matrix carrying its signature
  3. Symbolic preprocessing: the same as in F4, but the signatures of reducers must also propagate — when reducing with a multiple $x^\delta g$ of a basis element $g$, the new row's signature is $x^\delta \cdot \text{sig}(g)$
  4. Syzygy updates: the signature of a row that reduces to zero is added to the SyzygySet

Labeled rows: each row in the matrix is stored as (Signature, Vec<(coefficient, column)>) — a pairing of the signature with the sparse row data. During row-echelon computation the signature is preserved (row operations are linear combinations of rows, and the signature of the result is the maximum of the signatures of the participating rows).

Complexity Advantages of F5

In theory, the F5 signature criterion guarantees:

  • for regular sequences: no zero rows in the matrix, so the matrix size is minimized
  • for general ideals: a large reduction in zero rows, saving row-echelon cost

In practice the main bottlenecks are signature comparison and SyzygySet lookups. The oCAS implementation optimizes these operations with SmallVec and HashMap.

Equivalence of Monomial Orders and Change of Ordering

Different computational tasks require different monomial orders:

TaskOrder needed
EliminationLex
Solving polynomial systemsLex (combined with Sturm root isolation)
Ideal membership testingAny order works
Efficient computationGrevlex (smallest intermediate polynomials)

The standard strategy is "compute fast, then convert":

  1. Compute the basis with Grevlex (fastest — the intermediate polynomials have the lowest degrees)
  2. Convert to Lex (needed for elimination and solving)

Two conversion methods:

Method 1: recomputation (reorder). Reinterpret the polynomials of the Grevlex basis under the Lex order, then rerun F4 under Lex. For general ideals this is the default method — the basis elements may need additional reduction under the new order.

Method 2: FGLM (for zero-dimensional ideals only). The FGLM algorithm (detailed in the next subsection) performs the change of ordering for zero-dimensional ideals in $O(n \cdot D^3)$, far faster than recomputation.

The FGLM Algorithm

FGLM (Faugère–Gianni–Lazard–Mora 1993) provides an efficient change-of-ordering algorithm for zero-dimensional ideals.

Prerequisite: the ideal $I$ is zero-dimensional, i.e. $R/I$ is finite-dimensional as a $k$-vector space.

The Staircase and Normal Forms

Staircase: the set of monomials not divisible by any leading monomial $\text{lm}(g_i)$. For a zero-dimensional ideal the staircase is finite, of size $D = \dim_k(R/I)$.

The monomials of the staircase form a $k$-vector space basis of $R/I$. The normal form $f \bmod I$ of any polynomial $f$ can be represented uniquely as a $k$-linear combination of staircase monomials.

Algorithm Idea

FGLM enumerates monomials in increasing target order, computes their normal forms, and detects linear relations:

Input: GB_src (reduced Gröbner basis in the source order)
Output: GB_tgt (reduced Gröbner basis in the target order)

S ← {1}                    // processed monomials (in the staircase)
B ← {x1, x2, ..., xn}     // the border: "neighbors" of the processed monomials
GB_tgt ← ∅
seen_nf ← {}               // normal forms already seen

for m in increasing target order:
    nf ← normal_form(m, GB_src)  // compute the normal form under the source-order basis
    if nf ∈ span(seen_nf):        // linearly dependent?
        coeffs ← find_relation(seen_nf, nf)
        new_poly ← m - Σ coeffs_i · m_i
        GB_tgt ← GB_tgt ∪ {new_poly}
    else:
        S ← S ∪ {m}
        seen_nf ← seen_nf ∪ {nf}
        B ← B ∪ {m · xi : i = 1..n}  // expand the border
    if |S| = D:                     // the staircase is full
        break
return minimize(GB_tgt)

Key properties:

  • every linear dependence produces one polynomial of the target-order basis
  • every linear independence step extends the staircase, until its size reaches $D$
  • the total number of operations is $O(n \cdot D^3)$ — $D$ normal-form computations (each $O(D^2)$), plus $D$ Gaussian eliminations (each $O(D^2)$)

The non-zero-dimensional case: when the staircase is infinite, FGLM cannot terminate — fglm returns None, and reorder must be used instead.

Hilbert Functions and Regularity

The Hilbert function is a powerful tool for describing the structure of graded rings; in Gröbner basis computation it provides early-termination bounds.

Hilbert Functions and Hilbert Series

Definition. Let $I \subseteq R = \mathbb{F}[x_1, \ldots, x_n]$ be a homogeneous ideal. The Hilbert function

$$H_{R/I}(d) = \dim_\mathbb{F} (R/I)_d$$

measures the vector-space dimension of the degree-$d$ homogeneous component of the quotient ring.

The Hilbert series is the generating function of the Hilbert function:

$$\text{HS}{R/I}(t) = \sum{d=0}^{\infty} H_{R/I}(d) \cdot t^d$$

Theorem (Macaulay). The Hilbert series of $R/I$ depends only on the leading term ideal $\langle \text{lm}(g) : g \in \text{GB}(I) \rangle$, i.e.

$$\text{HS}{R/I}(t) = \text{HS}{R/\langle\text{LM}(I)\rangle}(t)$$

This means we can compute the Hilbert series from the leading terms of the Gröbner basis alone, without knowing any other information about the ideal.

Hilbert Series of Monomial Ideals

For an ideal $M$ generated by monomials $m_1, \ldots, m_s$, the inclusion–exclusion principle gives:

$$\text{HS}_{R/M}(t) = \frac{N(t)}{(1-t)^n}$$

where the numerator $N(t)$ is

$$N(t) = \sum_{k=0}^{s} (-1)^k \sum_{\substack{S \subseteq {1,\ldots,s} \ |S| = k}} t^{\deg \text{lcm}(S)}$$

Here $\text{lcm}(\emptyset) = 1$ (degree 0) and $\text{lcm}({m_i}) = m_i$.

Expansion:

$$N(t) = 1 - \sum_{i} t^{\deg m_i} + \sum_{i < j} t^{\deg \text{lcm}(m_i, m_j)} - \sum_{i < j < k} t^{\deg \text{lcm}(m_i, m_j, m_k)} + \cdots$$

Example. $M = \langle x^2, xy \rangle \subseteq \mathbb{F}[x, y]$ ($n = 2$).

  • $|S| = 0$: $t^0 = 1$
  • $|S| = 1$: $-t^2 - t^2 = -2t^2$ ($\deg(x^2) = \deg(xy) = 2$)
  • $|S| = 2$: $+t^{\deg \text{lcm}(x^2, xy)} = +t^{\deg(x^2 y)} = +t^3$

$$N(t) = 1 - 2t^2 + t^3$$

$$\text{HS}_{R/M}(t) = \frac{1 - 2t^2 + t^3}{(1-t)^2}$$

Regularity and Early Termination

Regularity is defined as the degree of the highest-degree nonzero term of the Hilbert numerator $N(t)$:

$$\text{reg}(M) = \max{d : [t^d] N(t) \neq 0}$$

Theorem (Bayer–Stillman). For a zero-dimensional ideal, the degrees of all polynomials in the Gröbner basis do not exceed $\text{reg}(M)$.

This means F4 can safely ignore critical pairs whose degree exceeds the regularity bound — they necessarily reduce to zero. The hilbert module of oCAS computes this bound via regularity_bound and uses it as an early-termination hint for F4 (an advisory bound; it does not affect correctness).

Complete Hilbert Series

The Hilbert series can also be computed for Gröbner bases of general (non-homogeneous) ideals:

$$\text{HS}_{R/I}(t) = \frac{N(t)}{(1-t)^n}$$

where $N(t)$ is given by the inclusion–exclusion formula for the leading term ideal. oCAS provides the HilbertSeries struct, supporting:

  • hilbert_function(d): $\dim_k (R/I)_d$ (by expanding the coefficient of $t^d$ in $\frac{N(t)}{(1-t)^n}$)
  • dimension(): when $N(1) \neq 0$ the dimension is $n - \text{ord}_{t=1}(N(t))$, i.e. the order of vanishing of $(1-t)$
  • degree(): the degree of the projective variety equals $h(1)$, where $h(t) = N(t)/(1-t)^{n-\dim}$ is the numerator after removing the factor $(1-t)^{n-\dim}$; for zero-dimensional ideals ($\dim = 0$) this equals $\dim_k(R/I)$ (the number of points)
  • hilbert_polynomial(): for sufficiently large $d$, $H_{R/I}(d)$ is a polynomial in $d$, computed by Lagrange interpolation

Implementation in oCAS

oCAS implements the three Gröbner basis algorithms and the Hilbert series computation in ocas-poly/src/groebner/. The unified entry point groebner_basis(ideal, Algorithm::Auto) currently routes to F4 by default.

f4.rs: The F4 Matrix Row-Echelon Algorithm

Gebauer–Moeller Critical Pair Management

The update_pairs function performs Gebauer–Moeller filtering each time a new polynomial is added to the basis:

  1. Generate candidates: pair the new polynomial with every existing element of the basis
  2. First criterion (monomial_are_coprime): if the leading monomials are coprime, skip directly (the product criterion)
  3. Second criterion (chain criterion, sequential elimination): if the $\text{lcm}$ of another (still live) candidate pair divides the current pair's $\text{lcm}$ (divisibility includes equality; duplicate pairs are dropped), the current pair is redundant and not kept
  4. Update criterion (for old pairs): an old pair is removed only when the new leading monomial divides its $\text{lcm}$ and both $\text{lcm}(i, \text{new})$ and $\text{lcm}(j, \text{new})$ are strictly smaller than $\text{lcm}(i,j)$; equality must be kept (otherwise S-polynomials required for completeness would be dropped)

The CriticalPair struct precomputes the $\text{lcm}$ and the monomial differences (the information needed to construct S-polynomials), avoiding repeated computation in later steps.

DivisorIndex: O(1)-ish Reducer Lookup

The core operation of symbolic preprocessing is "find the element of the basis whose leading monomial divides a given monomial". A naive implementation requires a linear scan costing $O(\text{monomials} \times |\text{basis}|)$. oCAS accelerates this operation with a support bitmask index.

support_mask: for an exponent vector $\alpha = (\alpha_1, \ldots, \alpha_n)$, define the bitmask

$$\text{mask}(\alpha) = \sum_{v : \alpha_v > 0} 2^v$$

i.e. bit $v$ is set if and only if the variable $x_v$ occurs in the monomial (variable indices up to 63).

Key observation: if $\text{lm}(g)$ divides $x^\alpha$, then $\text{support}(\text{lm}(g)) \subseteq \text{support}(x^\alpha)$. Hence a query only needs to enumerate the submasks of $\text{mask}(x^\alpha)$ and inspect the basis elements in the corresponding buckets.

The DivisorIndex structure:

#![allow(unused)]
fn main() {
struct DivisorIndex {
    // support_mask → list of basis element indices having this mask
    buckets: HashMap<u64, Vec<usize>>,
}
}

The query find_reducer(index, basis, exp):

  1. compute $m = \text{mask}(\text{exp})$
  2. enumerate all submasks $s \subseteq m$ of $m$
  3. for each basis element $g$ in buckets[s], check exactly whether $\text{lm}(g) \mid x^\text{exp}$
  4. among all reducers satisfying the condition, choose the one with the fewest terms (to speed up reduction)

Enumerating submasks costs $O(3^v)$ ($v$ = the number of variables), but in practice $v$ is usually small ($\leq 10$) and the buckets contain very few elements.

SimpCache: A Cache for Multiples of Reducers

During symbolic preprocessing, the same basis element may be used to reduce several different monomials with different differences. SimpCache<P> caches the computed polynomial multiples:

#![allow(unused)]
fn main() {
type SimpCache<P> = Vec<(SmallVec<[usize; 4]>, P)>;
}

get_simplified(cache, diff, basis_poly) checks the cache first and returns the cached value on a hit; otherwise it computes $x^{\text{diff}} \cdot \text{basis_poly}$ and stores it in the cache. The cache persists across rounds.

The ℤ_p Native Fast Path

When the coefficient domain is FiniteField(p) ($p < 2^{31}$), F4 uses FpPoly instead of the generic polynomial type:

#![allow(unused)]
fn main() {
struct FpPoly {
    terms: Vec<(i64, SmallVec<[usize; 4]>)>,  // (coeff, exponent)
    // coeff ∈ [0, p), sorted in descending order
}
}

All matrix operations run on i64 — no BigInt overhead. sub_scaled_fp implements O(nnz) sparse row subtraction with a two-pointer merge:

#![allow(unused)]
fn main() {
fn sub_scaled_fp(
    row: &mut Vec<(i64, usize)>,     // ascending column order
    pivot: &[(i64, usize)],           // ascending column order
    c: i64,                           // multiplier
    p: i64,                           // modulus
    scratch: &mut Vec<(i64, usize)>,  // scratch buffer
) {
    // two-pointer merge: row -= c * pivot
    // the first column (pivot column) is eliminated automatically (c = row[head_col] / pivot[head_col])
    // all results taken mod p; zero coefficients dropped
}
}

register_row_fp additionally caches the FpPoly representations of S-polynomials and reducer multiples for reuse across rounds.

The Matrix Elimination Flow

The complete F4 flow:

  1. Select critical pairs: take pairs from the queue sorted by $\text{lcm}$ size
  2. Symbolic preprocessing: expand the S-polynomial of each pair and all reducers, and build the column map
  3. Row echelonization: echelonize_fp (ℤ_p fast path) or echelonize_generic (generic)
  4. Extract new elements: convert the nonzero rows back to polynomials and check whether the leading term is "new"
  5. Update basis and pairs: add the new elements to the basis, and let update_pairs update the critical pair queue

f5.rs: The F5 Signature Algorithm

The Signature Structure

#![allow(unused)]
fn main() {
struct Signature {
    module_pos: usize,              // generator index k
    monomial: SmallVec<[usize; 4]>, // monomial exponents
}
}

Signatures are compared with the pot (position-over-term) order: first compare module_pos (smaller first), then compare monomial with the monomial order O.

LabeledPoly: Basis Elements with Signatures

#![allow(unused)]
fn main() {
struct LabeledPoly<D: Domain, O: MonomialOrder> {
    poly: SparseMultivariatePolynomial<D, O>,
    sig: Signature,
}
}

Every basis element carries a signature. When reducing with a multiple $x^\delta g$ of $g$, the signature of the new polynomial is $x^\delta \cdot \text{sig}(g)$.

SyzygySet: Tracking Reductions to Zero

#![allow(unused)]
fn main() {
struct SyzygySet {
    // module_pos → list of leading monomials of the known syzygies at that position
    lms: HashMap<usize, Vec<SmallVec<[usize; 4]>>>,
}
}

insert(sig): when a matrix row reduces to zero, its signature $(k, t)$ is recorded — $t$ is stored in lms[k].

contains(sig): test a signature $(k, t)$ — if there exists $s$ in lms[k] with $s \mid t$, it is a syzygy. monomial_divides(t, s) performs the componentwise comparison ($t_i \geq s_i$ for all $i$).

Matrix Construction and Elimination

The build_and_reduce flow of F5:

  1. Select pairs: critical pair selection similar to F4 (shares the update_pairs implementation)
  2. Syzygy filtering: before a pair enters the matrix, check its signature — if it is a syzygy, skip it
  3. Labeled rows: each row carries (Signature, Vec<(coefficient, column)>)
  4. Symbolic preprocessing: the same column-map construction as in F4, but the signatures of reducers must propagate
  5. Row echelonization: sort the rows in ascending signature order and run Gaussian elimination
  6. Extract new elements: nonzero rows are extracted as LabeledPoly, keeping their signatures
  7. Syzygy updates: the signatures of zero rows are added to SyzygySet

The ℤ_p Fast Path

F5 reuses F4's infrastructure — FpPoly, CriticalPair, update_pairs — and adds signature labels via LabeledFpPoly and LabeledFpRow. The structure of f5_fp is completely symmetric to the generic f5 — only the polynomial representation switches from a generic domain to i64 modular arithmetic.

hilbert.rs: Hilbert Series Computation

Implementing Inclusion–Exclusion

hilbert_numerator(generators) computes the Hilbert numerator of the monomial ideal $\langle m_1, \ldots, m_s \rangle$:

#![allow(unused)]
fn main() {
pub fn hilbert_numerator(generators: &[Vec<usize>]) -> Vec<(usize, i64)> {
    // iterate over all subsets S ⊆ {0, ..., s-1}
    // |S| = k contributes (-1)^k · t^{deg lcm(S)}
    // returns a sparse representation as (degree, coefficient) pairs
}
}

All $2^s$ subsets are enumerated (practical limit around $s \leq 20$); for each subset the degree of the $\text{lcm}$ is computed (componentwise maximum, then summed) and accumulated with its sign at the corresponding degree.

The Regularity Bound

regularity_bound(generators) returns the degree of the highest-degree nonzero term of the Hilbert numerator:

$$\text{reg} = \max{d : [t^d] N(t) \neq 0}$$

F4 uses this bound as an early-termination hint — when selecting critical pairs, a pair with $\deg(\text{lcm}) > \text{reg}$ can be safely skipped. This does not affect correctness (only efficiency) and is only valid for zero-dimensional ideals.

Complete Hilbert Series

hilbert_series(gb) computes the complete Hilbert series $\frac{N(t)}{(1-t)^n}$ of $R/I$ from a Gröbner basis:

  1. Extract leading terms: take the leading exponent vector of each polynomial in the basis
  2. Inclusion–exclusion: call hilbert_numerator to obtain $N(t)$
  3. Expand the series: $\frac{N(t)}{(1-t)^n} = N(t) \cdot \sum_{k \geq 0} \binom{n+k-1}{k} t^k$

Methods of the HilbertSeries struct:

MethodComputation
hilbert_function(d)expand the coefficient of $t^d$ in $\frac{N(t)}{(1-t)^n}$
dimension()$n$ when $N(1) \neq 0$; otherwise check the order of vanishing of $(1-t)$ recursively
degree()differentiate $N(t)$ $(n-\dim)$ times, evaluate at $t=1$, and divide by $(n-\dim)!$; for zero dimension ($\dim=0$) this equals $\dim_k(R/I) = \text{HS}(1)$ (the number of points)
hilbert_polynomial()Lagrange interpolation: evaluate $H(d)$ at sufficiently many $d$ and fit a polynomial

binomial_general(n, k) computes the generalized binomial coefficient $\binom{n+k-1}{k}$ ($n$ may be negative or zero), used to expand $(1-t)^{-n}$.

fglm.rs: The Zero-Dimensional Change-of-Ordering Algorithm

Computing the Staircase

compute_staircase(lms, n_vars) enumerates the monomials not divisible by any leading monomial:

  1. start a BFS at $1 = x_1^0 \cdots x_n^0$
  2. for each monomial $m$, check whether it is divisible by some $\text{lm}(g_i)$
  3. monomials that are not divisible are added to the staircase, and all their "neighbors" ($m \cdot x_i$) enter the queue
  4. if the staircase has not stopped growing after exceeding a threshold, return None (positive-dimensional ideal)

Computing Normal Forms

normal_form_monomial(m, gb, staircase, domain) computes the normal form of a monomial $m$ under the source-order basis, represented as a $k$-vector in staircase coordinates:

$$m \bmod I = \sum_{i=1}^{D} c_i \cdot s_i$$

where $s_1, \ldots, s_D$ are the staircase monomials. The computation is the standard multivariate division algorithm.

Detecting Linear Dependences

find_relation(seen, nf, domain) tests whether the new normal form nf lies in the span of the normal forms already seen. It uses incremental Gaussian elimination: maintain a matrix in row-echelon form and try to reduce each new vector — if it reduces to zero, a relation has been found.

Border Management

mark_multiples(visited, m, n_vars, max_deg) marks all multiples of a monomial $m$ (up to total degree max_deg) as visited, avoiding duplicate insertion into the border. It uses recursive enumeration.

The Unified Entry Point and Algorithm Selection

#![allow(unused)]
fn main() {
pub fn groebner_basis<D: Domain + 'static, O: MonomialOrder>(
    ideal: &[SparseMultivariatePolynomial<D, O>],
    algo: Algorithm,
) -> GroebnerBasis<D, O>
}

The Algorithm enum:

VariantBehavior
Autocurrently routes to F4; in the future the F5 crossover point will be calibrated on cyclic-n benchmarks
Buchbergerclassical S-polynomial iteration (for teaching)
F4batched matrix row echelon
F5signature criterion + matrix construction

All paths ultimately output a reduced Gröbner basis: first minimize (remove redundant elements whose leading terms are divisible by the leading term of another element), then auto_reduce (mutually reduce the tail terms).


Advanced Topics

Practical Performance: F4 vs Buchberger

oCAS's benchmarks (ocas-tests/benches/groebner.rs) compare Buchberger and F4 on the cyclic-n ideals:

BenchmarkBuchbergerF4Notes
cyclic-3 ($\mathbb{Q}$)feasiblefastBuchberger is already slow at this size
cyclic-4 ($\mathbb{Q}$)too slowfeasibleonly F4 is practical
cyclic-3/4/5 ($\mathbb{Z}{13}$, $\mathbb{Z}{101}$)—fastthe i64 fast path extends to cyclic-5

F5's signature criterion can in principle avoid reductions to zero and has the potential for order-of-magnitude speedups on hard ideals, but the oCAS F5 implementation is recent and has no official benchmark data yet.

Practical advice: use Algorithm::Auto (currently = F4) for general ideals; for systems known to be regular sequences, explicitly specifying Algorithm::F5 may be faster.

Theoretical Guarantees of F5

Theorem. The F5 algorithm is "optimal" in the following sense:

  1. No redundant reductions: for regular sequences, no row of the matrix reduces to zero
  2. Completeness: it produces exactly the same reduced Gröbner basis as F4
  3. Signature uniqueness: the signature of each polynomial uniquely determines its origin

These guarantees make F5 the theoretical first choice for hard ideals, although in practice F4's simpler implementation is often faster on small and medium problems.

Applications of the Hilbert Series

The Hilbert series is not merely a theoretical tool; it has the following practical applications in oCAS:

  1. Early termination: the regularity bound $\text{reg}(M)$ provides a reliable degree bound for F4
  2. Dimension testing: the Krull dimension of $R/I$ equals the order of the pole of $\text{HS}(t)$ at $t=1$
  3. Degree computation: the degree of the projective variety equals $N(1)$ (for zero-dimensional ideals, the staircase size)
  4. Zero-dimensionality test: $N(1) \neq 0$ if and only if the ideal is zero-dimensional

Gröbner Bases in Computational Algebraic Geometry

Gröbner bases are the universal tool of computational algebraic geometry. In oCAS they support the following applications (see FGLM and Elimination Theory and Gröbner Basis Implementation for details):

  • Solving polynomial systems: Gröbner basis → elimination → univariate equation → root isolation
  • Ideal operations: membership testing, sum, product, quotient, saturation, intersection, elimination, radical, primary decomposition
  • Geometric properties of varieties: dimension, degree, irreducible components

References

  1. Cox, D., Little, J. & O'Shea, D. Ideals, Varieties, and Algorithms. 4th ed., Springer, 2015.

    • Chapter 2: definition of Gröbner bases, Buchberger's algorithm, Buchberger's criteria
    • Chapter 3: the elimination theorem and ideal operations
    • Chapter 8: FGLM and change of ordering
  2. Adams, W.W. & Loustaunau, P. An Introduction to Gröbner Bases. AMS, 1994.

    • Chapters 4–5: detailed analysis of Buchberger's algorithm, Gebauer–Moeller optimizations
  3. Faugère, J.-C. "A New Efficient Algorithm for Computing Gröbner Bases (F4)." Journal of Pure and Applied Algebra, 139(1–3):61–88, 1999.

    • The original F4 paper: batched matrix row echelon
  4. Faugère, J.-C. "A New Efficient Algorithm for Computing Gröbner Bases without Reduction to Zero (F5)." ISSAC 2002, pp. 75–83.

    • The original F5 paper: the signature criterion and regular sequences
  5. Faugère, J.-C., Gianni, P., Lazard, D. & Mora, T. "Efficient Computation of Zero-dimensional Gröbner Bases by Change of Ordering." Journal of Symbolic Computation, 16(4):329–344, 1993.

    • The original FGLM paper
  6. Gebauer, R. & Möller, H.M. "On an Installation of Buchberger's Algorithm." Journal of Symbolic Computation, 6(2–3):275–286, 1988.

    • The Gebauer–Moeller critical pair filtering criteria
  7. Eder, C. & Perry, J. "Signature-based Algorithms to Compute Gröbner Bases." ISSAC 2009, pp. 139–146.

    • Eder–Perry's formalization of F5
  8. Bayer, D. & Stillman, M. "A Criterion for Detecting m-Regularity." Inventiones Mathematicae, 87(1):1–11, 1987.

    • The relation between regularity bounds and Hilbert functions
  9. Gathen, J. von zur & Gerhard, J. Modern Computer Algebra. 3rd ed., Cambridge University Press, 2013.

    • Chapter 21: a comprehensive survey of Gröbner bases

See also:

Advanced: Number Theory Algorithms

This chapter systematically explains the core number theory algorithms implemented in oCAS: primality testing, integer factorization, discrete logarithms, the Chinese remainder theorem, quadratic residues, and rational reconstruction. Each algorithm starts from its mathematical principle and goes into the concrete implementation details in oCAS.

Prerequisites

Before reading this chapter, the reader should be familiar with the following concepts:

  • Primes and divisibility: definition of primes, greatest common divisors, the Euclidean algorithm, the extended Euclidean algorithm
  • Modular arithmetic: congruences, modular inverse (computing $a^{-1} \bmod m$ with the extended Euclidean algorithm), fast modular exponentiation (binary exponentiation)
  • Euler's φ function: $\varphi(n) = n \prod_{p \mid n}(1 - 1/p)$, counting the integers in $[1, n]$ coprime to $n$
  • Multiplicative functions: $f(mn) = f(m)f(n)$ when $\gcd(m, n) = 1$; completely multiplicative when this holds for all $m, n$
  • Fermat's little theorem: $a^{p-1} \equiv 1 \pmod{p}$ for prime $p$ with $\gcd(a, p) = 1$
  • Basics of group theory: cyclic groups, the order of an element, generators (primitive roots)

Recommended reading: Shoup, A Computational Introduction to Number Theory and Algebra, Ch.1–9.

Basic Concepts

The Primality Testing Problem

Given a positive integer $n$, decide whether $n$ is prime. This is a decision problem, but deterministic algorithms (such as AKS) are far slower in practice than probabilistic ones. Modern practice uses probable prime tests: the probability that a composite passes the test can be made extremely small, but it cannot be absolutely ruled out.

The Integer Factorization Problem

Given a composite $n$, find a nontrivial factor $d$ ($1 < d < n$). Repeating recursively until all factors are prime yields $n = p_1^{e_1} \cdots p_k^{e_k}$. The difficulty of factorization is the foundational assumption behind the security of public-key cryptography such as RSA.

The Discrete Logarithm Problem

In the group $\mathbb{Z}_p^*$, given $g$ and $h$, find $x$ such that $g^x \equiv h \pmod{p}$. When $p - 1$ is smooth (has only small prime factors), the Pohlig–Hellman algorithm solves this efficiently.

Quadratic Residues

For an odd prime $p$ and an integer $a$, if there exists $x$ with $x^2 \equiv a \pmod{p}$, then $a$ is called a quadratic residue modulo $p$. The Legendre symbol $\left(\frac{a}{p}\right)$ equals $1$ for a residue, $-1$ for a non-residue, and $0$ when $p \mid a$.

Multiplicative Number-Theoretic Functions

FunctionDefinitionMultiplicative
$\varphi(n)$$#{k \in [1,n] : \gcd(k,n)=1}$yes
$\mu(n)$$1$ ($n=1$), $(-1)^k$ ($n$ has exactly $k$ distinct prime factors), $0$ ($n$ has a square factor)yes
$\tau(n)$number of positive divisors $= \prod(e_i + 1)$yes
$\sigma_k(n)$$\sum_{d \mid n} d^k$yes
$\lambda(n)$Liouville function $= (-1)^{\Omega(n)}$yes (completely multiplicative)

Core Theory

BPSW Primality Testing

The BPSW test (Baillie–Pomerance–Selfridge–Wagstaff) combines two independent probable prime tests: a base-2 strong Miller–Rabin test plus a strong Lucas probable prime test. Since it was proposed in 1980, no composite number is known to pass the BPSW test (it has been exhaustively verified for $n < 2^{64}$).

The Miller–Rabin Test

Principle: write $n - 1 = d \cdot 2^r$ ($d$ odd). For a base $a$, compute $a^d \bmod n$. If the result is $1$, or $-1$ appears in the sequence $a^d, a^{2d}, a^{4d}, \ldots, a^{d \cdot 2^{r-1}} \pmod{n}$, then $n$ passes this round.

Guarantee: if $n$ is prime, it passes for every $1 < a < n$. If $n$ is composite, at least $3/4$ of the bases are witnesses, i.e. they expose $n$. After testing $k$ random bases, the probability that a composite passes all of them is at most $4^{-k}$.

Determinism: for $n < 3.317 \times 10^{24}$, the Miller–Rabin test with the fixed base set ${2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}$ is deterministic. oCAS uses these 12 bases as the MR_WITNESSES constant.

The Strong Lucas Probable Prime Test

Lucas sequences: for parameters $(P, Q)$, define

$$U_0 = 0,\quad U_1 = 1,\quad U_{m+1} = P \cdot U_m - Q \cdot U_{m-1}$$

$$V_0 = 2,\quad V_1 = P,\quad V_{m+1} = P \cdot V_m - Q \cdot V_{m-1}$$

Binary ladder: compute $(U_k, V_k, Q^k) \bmod n$ efficiently. Use the doubling formulas

$$U_{2m} = U_m \cdot V_m, \quad V_{2m} = V_m^2 - 2Q^m$$

and the increment formulas

$$2U_{m+1} = P \cdot U_m + V_m, \quad 2V_{m+1} = D \cdot U_m + P \cdot V_m$$

where $D = P^2 - 4Q$. Divisions are avoided through the half-mod operation modulo $n$ (for odd $n$, $\text{half_mod}(x, n) = y$ satisfies $2y \equiv x \pmod{n}$).

Selfridge parameter selection: choose the first $D \in {5, -7, 9, -11, 13, \ldots}$ with Jacobi symbol $\left(\frac{D}{n}\right) = -1$, then set $P = 1$ and $Q = (1 - D)/4$.

The strong Lucas test: write $n + 1 = d \cdot 2^r$ ($d$ odd); $n$ passes if and only if $U_d \equiv 0 \pmod{n}$ or there exists $0 \leq i < r$ with $V_{d \cdot 2^i} \equiv 0 \pmod{n}$.

The BPSW Combination

is_prime_bpsw(n):
    1. if n < 2, return false; if n equals a prime in {2,3,5,…,37}, return true
    2. if n is divisible by some prime in 2..37, return false
    3. if n is a perfect square, return false
    4. strong Miller–Rabin test (base 2)
    5. if it passes, run the strong Lucas PRP test (Selfridge parameters)
    6. return true only if both pass

In oCAS, is_prime() uses the full 12-base Miller–Rabin, while is_prime_bpsw() uses the single-base Miller–Rabin plus the Lucas test. The two complement each other: is_prime() is deterministic for $n < 3.317 \times 10^{24}$; is_prime_bpsw() is theoretically stronger (it combines two tests based on different principles).

The Integer Factorization Strategy

oCAS's factor_integer is a layered driver that combines several factorization algorithms into an escalating strategy.

Trial Division

First generate the list of primes $\leq 1000$ with the sieve of Eratosthenes and divide by each in turn. This step costs $O(\pi(1000)) = O(168)$, negligible, but it quickly strips off small factors.

$$n = \prod_{p \leq 1000} p^{e_p} \cdot C$$

The remaining cofactor $C$ has all prime factors $> 1000$.

The Pollard rho–Brent Variant

Principle: random collision detection based on the birthday paradox. Define a sequence $x_0, x_1, x_2, \ldots$ with $x_{i+1} = x_i^2 + c \pmod{n}$ ($c$ a random constant). Modulo $n$ the sequence eventually enters a cycle, while modulo a true factor $p$ a collision occurs earlier.

The core optimizations of the Brent variant:

  1. Exponential search: detect collisions at $2^k$-step intervals (Floyd's tortoise-and-hare checks at every step; Brent only checks at powers of two)
  2. Batched GCD: accumulate the product of several $(x_i - y_i)$ values and take a single $\gcd$, amortizing the expensive GCD
  3. Backtracking: when a batched GCD returns $n$ itself, retry the individual differences within that batch

Time complexity: expected $O(n^{1/4})$ modular multiplications. In practice it is very efficient for factors of moderate size (< 20 digits).

Pollard's $p - 1$ Method (Stage 1)

Principle: if $n$ has a factor $p$ and $p - 1$ is $B_1$-smooth (i.e. all prime power factors of $p - 1$ are $\leq B_1$), then $p - 1 \mid M$ where $M = \prod_{q \leq B_1} q^{\lfloor \log_q B_1 \rfloor}$. By Fermat's little theorem, for any $\gcd(a, p) = 1$:

$$a^M \equiv 1 \pmod{p} \implies p \mid \gcd(a^M - 1, n)$$

Implementation details:

  • Generate the primes $q \leq B_1$ with the sieve of Eratosthenes
  • Modular exponentiation per prime: $a \leftarrow a^{q^e} \bmod n$ ($q^e \leq B_1 < q^{e+1}$)
  • Check $\gcd(a - 1, n)$ after accumulating a certain number of steps
  • If $\gcd = n$ (all factors are $p-1$-smooth), the method fails
  • If $1 < \gcd < n$, a nontrivial factor is found

Limitation: requires $p - 1$ to be smooth. If $p - 1$ has a large prime factor, the method does not work.

Williams's $p + 1$ Method (Stage 1)

Principle: complementary to the $p - 1$ method. If $p + 1$ is $B_1$-smooth, use the Lucas $V$ sequence instead of modular exponentiation.

The Lucas $V$ sequence: take $Q = 1$ and choose $P$ randomly with $\left(\frac{P^2 - 4}{n}\right) = -1$ (Jacobi symbol). Compute $V_M \bmod n$, where $M = \prod_{q \leq B_1} q^{\lfloor \log_q B_1 \rfloor}$. If $p + 1$ is $B_1$-smooth, then $V_M \equiv V_0 = 2 \pmod{p}$, i.e. $p \mid \gcd(V_M - 2, n)$.

Parameter selection: choose $P$ at random and check $\text{jacobi}(P^2 - 4, n) = -1$. The oCAS implementation uses lucas_uv_mod (the Lucas chain code shared with BPSW).

Limitation: requires $p + 1$ to be smooth. It complements the $p - 1$ method (some $p$ satisfy one but not the other).

Lenstra's Elliptic Curve Method (ECM)

Principle: reduce integer factorization to a smoothness question about the order of an elliptic curve group. For a random elliptic curve $E/\mathbb{F}_p$, the group order $|E(\mathbb{F}_p)|$ is distributed roughly uniformly in the Hasse interval $[p + 1 - 2\sqrt{p}, p + 1 + 2\sqrt{p}]$ (Sato–Tate). ECM succeeds when the group order of some curve is $B_1$-smooth.

Key advantage: unlike rho and the $p \pm 1$ methods, ECM does not fix the group order range — each new curve gives a new group order. This makes ECM succeed with positive probability for factors of any size, with complexity depending on the smoothness of the smallest factor $p$ rather than on $n$ itself.

Suyama parameterization: construct a Montgomery curve from a random $\sigma \notin {0, 1, 5}$:

$$u = \sigma^2 - 5, \quad v = 4\sigma$$

$$A = \frac{(v - u)^3(3u + v)}{4u^3 v} - 2$$

The curve is $B y^2 = x^3 + A x^2 + x$ with base point $(u^3, v^3)$. The Suyama parameterization guarantees that the base point is always rational, and if during the construction a GCD of a denominator exposes a factor of $n$, it is returned immediately.

Montgomery coordinates: use projective coordinates $(X : Z)$ (omitting the $Y$ coordinate), with curve equation $By^2 = x^3 + Ax^2 + x$ and $x = X/Z$.

  • Doubling $[2](X : Z)$: computed with the constant $a_{24} = (A + 2)/4$, needing only 4 modular multiplications and 4 modular additions/subtractions
  • Differential addition $P + Q$ given $P - Q$: only 6 modular multiplications
  • Montgomery ladder: scalar multiplication $[k]P$ via alternating doublings and additions, always using the base point as the fixed difference

Stage 1 algorithm: compute $[M]P$ where $M = \prod_{q \leq B_1} q^{\lfloor \log_q B_1 \rfloor}$. Take GCDs during the computation; return as soon as a nontrivial factor is found.

The factor_integer Driver

factor_integer combines the above algorithms into an escalating strategy:

factor_integer(n):
    1. Trial division: strip off all factors p ≤ 1000
    2. For the cofactor C (if C > 1):
       a. if is_prime_bpsw(C), record it as a prime factor
       b. otherwise call find_factor(C) to split it, and recurse

The escalation strategy of find_factor(n):

  1. One Pollard rho–Brent attempt first (cheapest for small factors; no smoothness requirement)
  2. Loop (starting with $B_1 = 2000$):
    • Pollard $p - 1$ (stage 1, requires some factor with $p - 1$ $B_1$-smooth)
    • Williams $p + 1$ (stage 1, requires some factor with $p + 1$ $B_1$-smooth; complements $p - 1$)
    • ECM: number of curves $\approx B_1 / 550$ (clamped to 10–300)
    • if all fail, $B_1 \leftarrow B_1 \times 4$ and continue the loop

The curve budget of about $B_1 / 550$ is the standard empirical ratio for ECM smoothness search, ensuring enough random curves per $B_1$ level to cover the group orders in the Hasse interval.

Discrete Logarithms

Baby-Step Giant-Step (BSGS)

Problem: given $g, h, p$, find $x$ such that $g^x \equiv h \pmod{p}$ ($0 \leq x < m$, where $m$ is an upper bound on the order of $g$).

Algorithm: let $m = \lfloor \sqrt{\text{bound}} \rfloor + 1$.

  1. Baby steps: compute and store ${(j, g^j \bmod p) : 0 \leq j < m}$ in a HashMap
  2. Giant steps: compute $g^{-m} \bmod p$; for $i = 0, 1, \ldots, m-1$, check whether $h \cdot (g^{-m})^i \bmod p$ is in the table
  3. If a match $g^j = h \cdot g^{-im}$ is found, then $x = im + j$

Complexity: $O(\sqrt{m})$ modular multiplications in time, $O(\sqrt{m})$ in space.

oCAS's dlog_bsgs stores the baby steps in a HashMap<Integer, Integer> with $(j, g^j)$ key-value pairs, giving $O(1)$ lookups.

The Pohlig–Hellman Algorithm

Scenario: $p$ prime, $g$ of order $n = p - 1$, with $n$ smooth: $n = q_1^{e_1} \cdots q_k^{e_k}$ ($q_i$ small primes).

Core idea: decompose the problem of finding $x \bmod n$ into the subproblems of finding $x \bmod q_i^{e_i}$, then combine with CRT.

Prime-power decomposition: for each $q = q_i$, $e = e_i$, find $x \bmod q^e$:

Write $x = x_0 + x_1 q + x_2 q^2 + \cdots + x_{e-1} q^{e-1}$ ($0 \leq x_j < q$; recover one "digit" at a time).

  1. Let $g_0 = g^{n/q} \bmod p$, $h_0 = h^{n/q} \bmod p$. Then $g_0^{x_0} = h_0 \pmod{p}$. Use BSGS to find $x_0 \bmod q$.
  2. Update $h \leftarrow h \cdot g^{-x_0}$, and compute $h_1 = (h \cdot g^{-x_0})^{n/q^2}$. Find $x_1$.
  3. Continue until $x_{e-1}$.
  4. $x \bmod q^e = x_0 + x_1 q + \cdots + x_{e-1} q^{e-1}$.

CRT combination: combine the results for all $q_i^{e_i}$ with crt_many to obtain $x \bmod n$.

Complexity: BSGS per prime power costs $O(\sqrt{q_i})$, for a total of $O(\sum e_i \sqrt{q_i})$. When all $q_i$ are small, this is much faster than the direct BSGS cost of $O(\sqrt{n})$.

oCAS's dlog_pohlig_hellman requires $p$ to be prime. It uses factor_integer to factor $p - 1$ (reusing the integer factorization module), recovers the digits prime power by prime power with BSGS, and finally combines and verifies the result with crt_many.

The Chinese Remainder Theorem

The Classical CRT

Theorem: let $m_1, \ldots, m_k$ be pairwise coprime. Given the system of congruences

$$x \equiv r_i \pmod{m_i}, \quad i = 1, \ldots, k$$

there exists a unique solution $x \bmod M$ where $M = m_1 \cdots m_k$.

Construction: $x = \sum_i r_i \cdot M_i \cdot M_i^{-1}$, where $M_i = M / m_i$ and $M_i^{-1} = M_i^{-1} \bmod m_i$.

Generalization: Non-Coprime Moduli

oCAS's crt (pairwise combination) and crt_many (combining many congruences) do not require the moduli to be pairwise coprime.

Pairwise CRT: for $x \equiv r_1 \pmod{m_1}$ and $x \equiv r_2 \pmod{m_2}$:

  1. Compute $g = \gcd(m_1, m_2)$
  2. Check $r_1 \equiv r_2 \pmod{g}$ (the consistency condition)
  3. If inconsistent, return None
  4. Otherwise solve with the extended Euclidean algorithm; the combined modulus is $\text{lcm}(m_1, m_2)$

Combining many congruences crt_many: a left fold — start from the first congruence and combine with the next one at a time:

$$x \equiv r_1 \pmod{m_1} \xrightarrow{\text{combine}} x \equiv R_2 \pmod{\text{lcm}(m_1, m_2)} \xrightarrow{\text{combine}} \cdots$$

The final result is $(R, M)$ where $M = \text{lcm}(m_1, \ldots, m_k)$ and $0 \leq R < M$. If any step detects an inconsistency, the whole computation returns None.

Complexity: $k$ pairwise combinations, each involving a GCD computation of cost $O(\log M)$.

Quadratic Residues

The Legendre Symbol and the Jacobi Symbol

Legendre symbol: for an odd prime $p$,

$$\left(\frac{a}{p}\right) = \begin{cases} 0 & p \mid a \ 1 & a \text{ is a quadratic residue mod } p \ -1 & a \text{ is a quadratic non-residue mod } p \end{cases}$$

Euler's criterion: $\left(\frac{a}{p}\right) \equiv a^{(p-1)/2} \pmod{p}$.

Jacobi symbol: the generalization of the Legendre symbol to composite moduli. If $n = p_1^{e_1} \cdots p_k^{e_k}$, then

$$\left(\frac{a}{n}\right) = \prod_i \left(\frac{a}{p_i}\right)^{e_i}$$

Key property: the Jacobi symbol can be computed quickly via the law of quadratic reciprocity, without factoring $n$.

The Law of Quadratic Reciprocity

For odd primes $p, q$:

$$\left(\frac{p}{q}\right) \left(\frac{q}{p}\right) = (-1)^{\frac{p-1}{2} \cdot \frac{q-1}{2}}$$

Supplementary rules:

  • $\left(\frac{2}{n}\right) = (-1)^{(n^2 - 1)/8}$, i.e. $1$ when $n \equiv \pm 1 \pmod 8$, and $-1$ when $n \equiv \pm 3 \pmod 8$
  • $\left(\frac{-1}{n}\right) = (-1)^{(n-1)/2}$, i.e. $1$ when $n \equiv 1 \pmod 4$

Computational algorithm (oCAS's jacobi function):

jacobi(a, n):
    if n is even or non-positive, return 0
    repeat:
      1. 2-adic stripping: extract all factors of 2 from a, accumulating the sign with the mod-8 rule
      2. reciprocity flip: swap a ↔ n, adjusting the sign according to the mod-4 values of a and n
      3. a = a mod n
      4. if a == 0, return 0 (n > 1) or the accumulated sign (n == 1)

Complexity: $O(\log^2 n)$, the same order as the Euclidean algorithm.

The Tonelli–Shanks Algorithm

Find $x$ such that $x^2 \equiv a \pmod{p}$ ($p$ an odd prime, $\left(\frac{a}{p}\right) = 1$).

Fast path: when $p \equiv 3 \pmod{4}$,

$$x = a^{(p+1)/4} \bmod p$$

because $x^2 = a^{(p+1)/2} = a \cdot a^{(p-1)/2} = a \cdot \left(\frac{a}{p}\right) = a$.

The general case ($p \equiv 1 \pmod{4}$) — the Tonelli–Shanks algorithm:

  1. Decompose: write $p - 1 = Q \cdot 2^S$ ($Q$ odd)
  2. Find a non-residue: search for $z$ with $\left(\frac{z}{p}\right) = -1$
  3. Initialize:
    • $M = S$
    • $c = z^Q \bmod p$ (an element of order $2^{S-1}$)
    • $t = a^Q \bmod p$
    • $R = a^{(Q+1)/2} \bmod p$
  4. Loop (while $t \neq 1$):
    • find the smallest $i > 0$ such that $t^{2^i} \equiv 1 \pmod{p}$
    • update $c \leftarrow c^{2^{M-i-1}}$, $t \leftarrow t \cdot c^2$, $R \leftarrow R \cdot c$, $M \leftarrow i$
  5. Return $R$

Invariant: $R^2 = a \cdot t \pmod{p}$ and $c$ has order $2^M$. When the loop terminates $t = 1$, hence $R^2 \equiv a$.

Complexity: $O(S^2 \log p)$, where $S = v_2(p - 1)$ (the power of 2 dividing $p - 1$). On average $S$ is small, so the algorithm is efficient.

Rational Reconstruction

Problem: given $a \in \mathbb{Z}$ and a modulus $m$, find $n, d \in \mathbb{Z}$ such that

$$a \cdot d \equiv n \pmod{m}, \quad \gcd(n, d) = 1, \quad 2|n| \cdot |d| < m$$

Application: in the modular GCD algorithm, after combining the GCD coefficients from several $\mathbb{F}_p$'s with CRT, the result modulo $M$ must be "reconstructed" back to rational numbers over $\mathbb{Q}$.

The Wang/extended Euclidean algorithm:

Use the extended Euclidean algorithm tracking the sequence $(r_i, t_i)$:

  1. Initialize: $(r_0, r_1) = (m, a)$, $(t_0, t_1) = (0, 1)$
  2. Iterate: $q = \lfloor r_0 / r_1 \rfloor$, $(r_0, r_1) \leftarrow (r_1, r_0 - q \cdot r_1)$, $(t_0, t_1) \leftarrow (t_1, t_0 - q \cdot t_1)$
  3. Termination condition: stop when $|r_1| \leq \sqrt{m/2}$ and $|t_1| \leq \sqrt{m/2}$
  4. Verification: let $n = r_1$ and $d = t_1$ (take positive values); check $a \cdot d \equiv n \pmod{m}$ and $2|n| \cdot |d| < m$
  5. If verification fails or $t_1 = 0$, return None (no rational reconstruction satisfies the conditions)

Uniqueness theorem: when $2|n| \cdot |d| < m$, the pair $(n, d)$ satisfying the conditions is unique. This condition guarantees that the reconstructed result is the "simplest" rational representation.

Complexity: $O(\log m)$ GCD steps, the same order as the Euclidean algorithm.

Implementation in oCAS

oCAS's number theory algorithms live in the number_theory module of the ocas-domain crate and in the rational reconstruction module of ocas-poly.

Module Structure

ocas-domain/src/
├── number_theory.rs          ← primality tests, modular inverse, CRT, Legendre/Jacobi, mod_sqrt
└── number_theory/
    ├── primes.rs             ← BPSW, Lucas sequences (lucas_uv_mod, strong_lucas_prp)
    ├── factor.rs             ← the integer factorization driver (factor_integer) and its methods
    ├── dlog.rs               ← BSGS and Pohlig–Hellman
    ├── crt.rs                ← multi-congruence CRT (crt_many)
    └── functions.rs          ← φ, μ, τ, σ, λ

ocas-poly/src/
└── rational_reconstruction.rs ← rational reconstruction (extended Euclidean method)

Primality Testing

FunctionLocationDescription
is_prime(n)number_theory.rs12-base Miller–Rabin; deterministic for $n < 3.317 \times 10^{24}$
is_prime_bpsw(n)primes.rsbase-2 MR + strong Lucas PRP
is_prime_u64(n)primes.rsu64-specific; delegates to is_prime
next_prime(n)number_theory.rstest odd numbers one by one starting from $n + 1$
primes_from(n)number_theory.rsa prime iterator

Lucas sequence computation is implemented by lucas_uv_mod using the binary ladder, returning $(U_k, V_k, Q^k) \bmod n$. This function is shared by the BPSW test and Williams's $p + 1$ factorization method.

Integer Factorization

FunctionLocationDescription
factor_integer(n)factor.rsentry point: trial division + recursive splitting
factor_integer_with_rng(n, rng)factor.rsversion with an explicit RNG
factor_trial(n, limit)factor.rstrial division, returns (factor, cofactor)
pollard_rho_brent(n, rng)factor.rsBrent variant, with retries
pollard_pm1(n, b1, rng)factor.rsPollard $p - 1$, Stage 1
williams_pp1(n, b1, rng)factor.rsWilliams $p + 1$, Stage 1
ecm(n, b1, max_curves, rng)factor.rsLenstra ECM, Suyama parameterization

Internal implementation details:

  • primes_up_to(limit): the sieve of Eratosthenes, generating prime tables for trial division and smoothness bounds
  • prime_power_le(q, bound): computes $q^e \leq \text{bound}$, used to construct the smooth exponent $M$
  • ProjPoint { x, z }: a Montgomery projective-coordinate point
  • ecm_double, ecm_add, ecm_mul: group operations on the Montgomery curve
  • suyama_curve(sigma, n): the Suyama parameterization, returning the Suyama enum (Curve/Factor/Degenerate)
  • find_factor(n, rng): the escalation driver (rho → p−1 → p+1 → ECM, with increasing smoothness bounds)

Discrete Logarithms

FunctionLocationDescription
dlog_bsgs(base, target, modulus)dlog.rsbaby-step giant-step
dlog_pohlig_hellman(base, target, p)dlog.rsPohlig–Hellman (requires $p$ prime)

Internals:

  • bsgs_bounded(base, target, modulus, order_bound): the BSGS core, searching with a given order bound
  • dlog_bsgs sets the bound to modulus - 1 and delegates to bsgs_bounded
  • dlog_pohlig_hellman factors $p - 1$ with factor_integer, recovers the digits prime power by prime power with BSGS, and combines them with crt_many

The Chinese Remainder Theorem

FunctionLocationDescription
crt(r1, m1, r2, m2)number_theory.rspairwise combination; the moduli need not be coprime
crt_many(congruences)crt.rsleft-fold combination of many congruences

crt_many starts from the first congruence and combines with the next one at a time via crt. If any step is inconsistent, the whole computation returns None.

Quadratic Residues

FunctionLocationDescription
legendre(a, p)number_theory.rsthe Legendre symbol, delegating to jacobi
jacobi(a, n)number_theory.rsthe Jacobi symbol (quadratic reciprocity + 2-adic stripping)
mod_sqrt(a, p)number_theory.rsTonelli–Shanks, including the $p \equiv 3 \pmod{4}$ fast path

The mod_sqrt implementation:

  1. First check $\left(\frac{a}{p}\right) = 1$ (otherwise return None)
  2. If $p \equiv 3 \pmod{4}$, take the fast path $x = a^{(p+1)/4} \bmod p$
  3. Otherwise run the full Tonelli–Shanks algorithm: find a non-residue $z$, decompose $p - 1 = Q \cdot 2^S$, and loop maintaining the $(c, t, r, m)$ invariants

Auxiliary Number-Theoretic Functions

FunctionLocationDescription
euler_phi(n)functions.rs$\varphi(n) =
moebius_mu(n)functions.rs$\mu(n)$, based on factor_integer
divisor_tau(n)functions.rs$\tau(n) = \prod(e_i + 1)$
divisor_sigma(n, k)functions.rs$\sigma_k(n) = \prod \frac{p^{k(e+1)} - 1}{p^k - 1}$
liouville_lambda(n)functions.rs$\lambda(n) = (-1)^{\Omega(n)}$

All multiplicative functions first obtain the prime factorization via factor_integer and then evaluate the multiplicative formula.

Rational Reconstruction

FunctionLocationDescription
rational_reconstruction(a, m)ocas-poly/src/rational_reconstruction.rsthe Wang/extended Euclidean method

Internally it uses integer_sqrt to compute $\lfloor\sqrt{m/2}\rfloor$ as the threshold for the termination bound.

Basic Utility Functions

FunctionLocationDescription
mod_inv(a, m)number_theory.rsmodular inverse via the extended Euclidean algorithm
extended_gcd(a, b)number_theory.rsreturns $(g, x, y)$ with $ax + by = g$
symmetric_mod(a, m)number_theory.rsreduce into $(-m/2, m/2]$

References

  1. Shoup, V. A Computational Introduction to Number Theory and Algebra. Cambridge University Press, 2nd edition, 2009.

    • Ch.10: Primality testing (Miller–Rabin, randomized algorithms)
    • Ch.11: Finding discrete logarithms (BSGS, Pohlig–Hellman)
    • Ch.19: Factoring integers (Pollard rho, ECM)
  2. Crandall, R. & Pomerance, C. Prime Numbers: A Computational Perspective. Springer, 2nd edition, 2005.

    • Ch.3: Recognizing primes and composites (BPSW, Lucas tests)
    • Ch.5: Exponential factoring algorithms (ECM, Pollard rho)
    • Ch.6: Subexponential factoring (Pollard $p-1$, Williams $p+1$)
    • Ch.7: Modern discrete logarithm algorithms
  3. Brent, R. P. "An improved Monte Carlo factorization algorithm." BIT Numerical Mathematics, 20(2):176–184, 1980.

    • The Brent variant of the Pollard rho algorithm
  4. Williams, H. C. "A p+1 method of factoring." Mathematics of Computation, 39(159):225–234, 1982.

    • Williams's $p + 1$ method
  5. Lenstra, H. W. Jr. "Factoring integers with elliptic curves." Annals of Mathematics, 126(3):649–673, 1987.

    • The ECM method
  6. Montgomery, P. L. "Speeding the Pollard and elliptic curve methods of factorization." Mathematics of Computation, 48(177):243–264, 1987.

    • Montgomery curve coordinates and ladder optimizations
  7. Shanks, D. & Tonelli, R. See Cohen, H. A Course in Computational Algebraic Number Theory, Algorithm 1.5.1.

    • The Tonelli–Shanks modular square root algorithm
  8. Baillie, R. & Wagstaff, S. S. "Lucas pseudoprimes." Mathematics of Computation, 35(152):1391–1417, 1980.

    • The Lucas part of the BPSW test
  9. Pohlig, S. & Hellman, M. "An improved algorithm for computing logarithms over GF(p) and its cryptographic significance." IEEE Transactions on Information Theory, 24(1):106–110, 1978.

    • The Pohlig–Hellman discrete logarithm algorithm
  10. Wang, P. S. "A p-adic algorithm for univariate partial fractions." Proceedings of SYMSAC '81, 212–217, 1981.

    • The extended Euclidean method for rational reconstruction

Advanced: Symbolic Calculus

Prerequisites

  • Calculus basics: definition of the derivative, basic differentiation rules, the concept of Taylor series
  • Expression trees and recursive data structures (see Polynomial Algebra)
  • The oCAS expression system (Atom, AtomNode, AtomArena)

Suggested reading: Polynomial Algebra, Linear Algebra.


Basic Concepts

Symbolic Calculus on Expression Trees

In symbolic computation, a function $f(x)$ is represented as an expression tree. The leaf nodes are constants or variables; the internal nodes are operators (Add, Mul, Pow) or function calls (sin, exp, log, etc.).

For example, the expression tree of $f(x) = x^2 + \sin(x)$ is:

Add
├── Pow
│   ├── Var("x")
│   └── Num(2)
└── Fun("sin", [Var("x")])

The core idea of symbolic differentiation is pattern matching: for each node of the expression tree, apply the differentiation rule corresponding to its type, recursively building the derivative expression tree.

Basic Differentiation Rules

Given differentiable functions $f(x)$ and $g(x)$:

RuleFormula
Constant$\frac{d}{dx}[c] = 0$
Identity$\frac{d}{dx}[x] = 1$
Sum$\frac{d}{dx}[f + g] = f' + g'$
Product$\frac{d}{dx}[f \cdot g] = f' \cdot g + f \cdot g'$
Chain$\frac{d}{dx}[f(g(x))] = f'(g(x)) \cdot g'(x)$
Power (constant exponent $n$)$\frac{d}{dx}[f^n] = n \cdot f^{n-1} \cdot f'$
Exponential (constant base $a$)$\frac{d}{dx}[a^g] = a^g \cdot \ln a \cdot g'$
General power$\frac{d}{dx}[f^g] = f^g \cdot (\ln f \cdot g' + g \cdot f'/f)$

Derivatives of Elementary Functions

$f(u)$$f'(u)$
$\sin u$$\cos u$
$\cos u$$-\sin u$
$\exp u$$\exp u$
$\ln u$$u^{-1}$
$\sqrt{u}$$(2\sqrt{u})^{-1}$
$\tan u$$\sec^2 u$
$\sec u$$\sec u \cdot \tan u$
$\arctan u$$(1 + u^2)^{-1}$

Note: the table gives $f'(u)$ (the derivative with respect to $u$); in fact $\frac{d}{dx}[f(u(x))] = f'(u) \cdot u'(x)$, i.e. one still has to multiply by the chain-rule factor $u'(x)$.

Taylor Series

Suppose $f$ is infinitely differentiable in a neighbourhood of the point $a$. The Taylor series of $f$ at $a$ is:

$$ f(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(a)}{n!} (x - a)^n $$

where $f^{(n)}(a)$ is the $n$-th derivative of $f$ at $a$. Truncating at order $N$ yields the Taylor polynomial:

$$ T_N(x) = \sum_{n=0}^{N} \frac{f^{(n)}(a)}{n!} (x - a)^n $$

Common expansions (at $a = 0$, i.e. Maclaurin series):

$$ e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \cdots $$

$$ \sin x = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \cdots $$

$$ \cos x = 1 - \frac{x^2}{2!} + \frac{x^4}{4!} - \cdots $$

$$ \ln(1+x) = x - \frac{x^2}{2} + \frac{x^3}{3} - \cdots \quad (|x| < 1) $$


Core Theory

Recursive Differentiation on Expression Trees

The core algorithm of symbolic differentiation is structural recursion over the expression tree. For each node type, the corresponding differentiation rule is applied and the subexpressions are differentiated recursively.

Algorithm diff(expr, var):

  1. Constant Num(c): return Num(0).
  2. Variable Var(v): if $v = \text{var}$, return Num(1); otherwise return Num(0).
  3. Sum Add(a₁, a₂, …, aₙ): differentiate term by term, returning Add(diff(a₁), diff(a₂), …, diff(aₙ)).
  4. Product Mul(a₁, a₂, …, aₙ): apply the generalised product rule. For the product $a_1 a_2 \cdots a_n$ of $n$ factors:

$$ \frac{d}{dx}\left[\prod_{i=1}^n a_i\right] = \sum_{i=1}^n \left(\frac{d a_i}{dx} \cdot \prod_{j \neq i} a_j\right) $$

That is, for each position $i$, replace the $i$-th factor by its derivative, keep the other factors unchanged, and sum the $n$ results.

  1. Power Pow(base, exp): three cases —

    • Constant exponent $n$: $\frac{d}{dx}[b^n] = n \cdot b^{n-1} \cdot b'$
    • Constant base $a$: $\frac{d}{dx}[a^u] = a^u \cdot \ln a \cdot u'$
    • General case (generalised power rule): $\frac{d}{dx}[b^e] = b^e \cdot (\ln b \cdot e' + e \cdot b'/b)$
  2. Function call Fun(name, [u]): look up $f'(u)$ in the table, then multiply by $u'$ (chain rule).

  3. Unknown function: return the unevaluated form Derivative(f(x), x).

Key property: because oCAS expressions use hash-consing (structural sharing), the intermediate expressions produced by recursive differentiation automatically share identical subtrees. After differentiation, the rewrite engine simplifies the result (e.g. $0 \cdot f \to 0$, $1 \cdot f \to f$), eliminating redundant terms.

The Recurrence Algorithm for Taylor Expansion

A direct implementation of Taylor expansion can exploit successive differentiation:

Algorithm taylor(expr, var, point, order):

  1. Let current = expr, sum = 0.
  2. For $n = 0, 1, \dots, \text{order}$:
    • Compute $f^{(n)}(\text{point})$: replace var in current by point (i.e. evaluate).
    • Compute the coefficient $c_n = f^{(n)}(\text{point}) / n!$.
    • If $n = 0$ the term is $c_0$; otherwise the term is $c_n \cdot (x - \text{point})^n$.
    • Accumulate: $\text{sum} \mathrel{+}= \text{term}$.
    • If $n < \text{order}$, update current = diff(current, var).
  3. Simplify and return sum.

Handling factorials: $1/n!$ is represented as a power $n!^{-1}$ to avoid introducing floating-point numbers. The mul_by_factorial_inverse function computes $n!$ (using i64 multiplication, safe for $n \leq 20$) and then constructs $c \cdot (n!)^{-1}$.

Complexity: in the loop $n = 0, \dots, N$, each step performs one substitution (evaluation), but a symbolic differentiation is performed only when $n < N$. Symbolic differentiation itself is $O(|\text{expr}|)$ ($|\text{expr}|$ being the size of the expression tree), but simplification may add nodes. Overall, a Taylor expansion of order $N$ requires $N$ differentiations and $N+1$ substitutions.

Semantics of Substitution

substitute(expr, var, replacement) replaces every free occurrence of the variable var in the expression by replacement.

Implementation strategy: oCAS uses a bottom-up transform — the transform function walks the expression tree, and for each leaf node checks whether it is the variable to be replaced. If so it returns replacement; otherwise the node is left unchanged.

Deep copy vs. references:

  • oCAS's Atom is a Copy handle (a pointer) into an arena, not an owning value. transform allocates new nodes on the arena to build the result, but unmodified subtrees reuse the original handles (zero copy).
  • This makes substitute purely functional: the original expression is unchanged, and the result is a newly allocated expression tree in which the replaced paths point to new nodes while unaffected subtrees are shared with the original tree.
  • Unlike "destructive modification", this guarantees referential transparency: substituting into the same expression repeatedly does not interfere.

Limitations of substitution: substitute is a syntactic substitution, not a semantic one. It replaces the literal variable name, not its "value". For example, replacing $t \to y$ inside $\int_0^x f(t), dt$ would wrongly change the integration variable — such semantic replacements require finer-grained binding analysis.


Implementation in oCAS

diff: Symbolic Differentiation

File: ocas-calc/src/derivative.rs

#![allow(unused)]
fn main() {
pub fn diff<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a>
}

Structure of the implementation:

diff is the entry point: it calls the internal diff_raw to perform the actual recursion, then simplifies the result with simplify, and finally normalizes it with normalize.

#![allow(unused)]
fn main() {
pub fn diff<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a> {
    let rules = calculus_rules(ctx, &crate::pattern_alloc::VecAlloc);
    let raw = diff_raw(ctx, expr, var);
    let simplified = simplify(ctx, raw, &rules, 20);
    normalize(ctx, simplified)
}
}

diff_raw pattern-matches on every variant of AtomNode:

  • Num(_): constant, derivative 0.
  • Var(v): derivative 1 if v == var, otherwise 0.
  • Add(args): call diff_raw on each argument and collect the results into a new Add node.
  • Mul(args): the generalised product rule — for $n$ factors, generate $n$ product terms (each with one factor replaced by its derivative), then add them.
  • Pow(base, exp): three cases (constant exponent / constant base / general), as above.
  • Fun(name, args): delegate to diff_function.

diff_function maintains a hard-coded derivative table covering the eight functions sin, cos, exp, log, sqrt, tan, sec, atan. For each function $f(u)$ the table stores $f'(u)$ (the derivative with respect to $u$), which is then multiplied by the chain-rule factor $\frac{du}{dx}$.

For functions not in the table, an unevaluated form is returned:

#![allow(unused)]
fn main() {
ctx.fun("Derivative", &[ctx.fun(name_str, args), ctx.var(var.as_str())])
}

namely Derivative(f(x), x), representing a derivative that could not be computed automatically.

Simplification pipeline: the raw derivative produced by diff may contain redundancy (such as 0 * sin(x), 1 * cos(x)). simplify runs calculus_rules (calculus-specific rules plus the default simplification rules) for at most 20 rounds of fixpoint simplification, and then normalize brings the result into canonical form.

taylor: Taylor Expansion

File: ocas-calc/src/series.rs

#![allow(unused)]
fn main() {
pub fn taylor<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    point: Atom<'a>,
    order: usize,
) -> Atom<'a>
}

Implementation details:

  1. Precompute (x - point) as the common subexpression x_minus_p.
  2. Loop $n = 0, \dots, \text{order}$:
    • substitute(ctx, current, var, point) evaluates $f^{(n)}(a)$.
    • mul_by_factorial_inverse computes $f^{(n)}(a) / n!$.
    • Build the term $c_n \cdot (x - a)^n$ (the power factor is omitted for $n = 0$).
    • Accumulate into sum.
    • If $n < \text{order}$, current = diff(ctx, current, var) computes the next derivative.
  3. Simplify and normalize.

mul_by_factorial_inverse:

#![allow(unused)]
fn main() {
fn mul_by_factorial_inverse<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, n: usize) -> Atom<'a> {
    if n == 0 { return expr; }
    let mut fact: i64 = 1;
    for i in 2..=n {
        fact = fact.checked_mul(i as i64).expect("factorial fits in i64");
    }
    ctx.mul(&[expr, ctx.pow(ctx.num(fact), ctx.num(-1))])
}
}

$1/n!$ is represented as $(n!)^{-1}$, i.e. Pow(Num(n!), Num(-1)), preserving an exact rational representation. checked_mul guarantees $n \leq 20$ ($20! = 2,432,902,008,176,640,000 < 2^{63}$).

Example:

#![allow(unused)]
fn main() {
// expand e^x at x=0 to order 3
let x = ctx.var("x");
let expr = ctx.fun("exp", &[x]);
let result = taylor(&ctx, expr, Symbol::new("x"), ctx.num(0), 3);
// result = "1 + x + ((2^-1)*(x^2)) + ((6^-1)*(x^3))"
// i.e. 1 + x + x²/2 + x³/6

// expand sin(x) at x=0 to order 5
let sin_x = ctx.fun("sin", &[x]);
let result = taylor(&ctx, sin_x, Symbol::new("x"), ctx.num(0), 5);
// result = "x + (-1*(6^-1)*(x^3)) + ((120^-1)*(x^5))"
// i.e. x - x³/6 + x⁵/120 (the even-order coefficients vanish automatically)
}

substitute: Variable Substitution

File: ocas-calc/src/series.rs

#![allow(unused)]
fn main() {
pub fn substitute<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    replacement: Atom<'a>,
) -> Atom<'a>
}

Implementation:

#![allow(unused)]
fn main() {
pub fn substitute<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    replacement: Atom<'a>,
) -> Atom<'a> {
    transform(ctx, expr, |a| match a.node() {
        AtomNode::Var(v) if *v == var => Some(replacement),
        _ => None,
    })
}
}

The transform function (bottom-up transform) walks the expression tree. The closure checks each node: if it is Var(v) with v == var, it returns Some(replacement) (replace); otherwise None (leave unchanged).

Key properties:

  • Purely functional: the original expression is not modified; a new tree is returned. Unmodified subtrees are shared through Atom's Copy semantics.
  • Deep substitution: all free occurrences are replaced, including those nested in function arguments (such as the x in sin(x)).
  • No capture check: there is no check whether the substitution causes variable capture — this is a syntactic-level substitution.

integrate: Symbolic Integration

File: ocas-calc/src/integral/mod.rs

#![allow(unused)]
fn main() {
pub fn integrate<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Atom<'a>
}

oCAS's integrator uses a layered pipeline architecture (see Symbolic Integration Algorithms and The Risch Algorithm):

  1. Direct lookup layer (integrate_raw): direct integration rules for constants, variables, sums, and simple products/powers/functions.
  2. Rational function layer (rational.rs): partial fraction decomposition + Hermite reduction + the logarithmic part (logarithmic-derivative identity, completing the square, Rothstein–Trager).
  3. Risch algorithm layer (risch.rs): structural theorems over the differential field tower $\mathbb{Q}(x, t_1, \dots, t_n)$.
  4. Trigonometric layer (trig.rs): rewriting via Euler's formula into complex exponentials ($t = e^{ix}$) → retry Risch on the rewritten form → realify back to real form.
  5. Special functions layer (special.rs): tables of special functions such as erf, erfi, Ei, Si, Ci, Shi, Chi, Fresnel.
  6. Heuristic layer (heuristic.rs): integration by parts (LIATE ordering), trigonometric substitution ($\sqrt{a^2 - x^2} \to a\sin\theta$), Weierstrass rationalization $t = \tan(x/2)$, Euler substitution.
  7. Fallback: return the unevaluated form Integral(expr, var).

The integrate_with_fuel variant accepts a Fuel budget to prevent infinite loops in the simplification phase. The integration traversal itself is bounded by the internal recursion depth limit MAX_DEPTH = 8 (the Risch layer has its own MAX_RISCH_DEPTH = 16):

#![allow(unused)]
fn main() {
pub fn integrate_with_fuel<'a>(
    ctx: &'a AtomArena<'a>,
    expr: Atom<'a>,
    var: Symbol,
    fuel: &Fuel,
) -> Result<Atom<'a>>
}

Advanced Topics

The Interaction of Differentiation and Simplification

The correctness of diff depends not only on the differentiation rules themselves but also on the quality of simplification. For example:

  • $\frac{d}{dx}[x^2 + 3x] = 2x + 3$ — this requires simplifying $1 \cdot 3$ to $3$.
  • $\frac{d}{dx}[e^{\ln x}] = e^{\ln x} \cdot \frac{1}{x} = 1$ — this requires simplifying $e^{\ln x} \to x$.

oCAS handles such cases through multi-round simplification with calculus_rules and the default rules. However, some identities (such as $\sin^2 x + \cos^2 x = 1$) may require dedicated trigonometric simplification rules.

Unevaluated Derivatives and Integrals

When diff encounters an unknown function (a function not in the derivative table), it returns Derivative(f, x). Similarly, integrate returns Integral(f, x) for integrals it cannot handle.

These unevaluated forms are valid expression nodes — they can participate in subsequent symbolic operations. For example, $\frac{d}{dx}[\text{Derivative}(f, x)]$ returns Derivative(Derivative(f, x), x), representing the second derivative.

Bridging to Numerical Evaluation

Expressions produced by symbolic differentiation can be passed to the ExpressionEvaluator (see Evaluation and JIT) for efficient numerical evaluation. A typical workflow:

  1. Symbolic differentiation: diff(&ctx, f, x) → the derivative expression $f'(x)$.
  2. Compile the evaluator: ExpressionEvaluator::compile(f').
  3. Numerical evaluation: evaluator.evaluate(&[x_val]) → $f'(x_{\text{val}})$.

This is far more accurate than numerical differentiation (finite differences) and avoids the problem of step-size selection.


References

  1. Geddes, K. O., Czapor, S. R. & Labahn, G. Algorithms for Computer Algebra. Kluwer Academic Publishers, 1992. — Chapter 11, "Symbolic Differentiation", covers recursive differentiation on expression trees, simplification strategies, and derivative-table design.
  2. Bronstein, M. Symbolic Integration I: Transcendental Functions. Springer, 2005. — The authoritative reference on the Risch algorithm and the design basis of oCAS's integrator.
  3. Cohen, H. A Course in Computational Algebraic Number Theory. Springer, 1993. — Chapter 1 contains general methodology for complexity analysis of algorithms.

Advanced: ODE Solving Theory

Prerequisites


Basic Concepts

Definition of an Ordinary Differential Equation

An ordinary differential equation (ODE) is an equation involving an unknown function $y(x)$ and its derivatives. The general form is

$$F!\left(x,, y,, y',, y'',, \dots,, y^{(n)}\right) = 0$$

where $y^{(k)} = \dfrac{d^k y}{dx^k}$.

Order

The highest-order derivative appearing in the ODE determines the order of the equation. For example:

EquationOrder
$y' + y = 0$1
$y'' + 2y' + y = \sin x$2
$y''' - y = 0$3

Linear and Nonlinear

An $n$-th order ODE is linear if and only if it can be written as

$$a_n(x), y^{(n)} + a_{n-1}(x), y^{(n-1)} + \cdots + a_1(x), y' + a_0(x), y = g(x)$$

where the $a_k(x)$ and $g(x)$ depend only on the independent variable $x$, not on $y$. If $g(x) \equiv 0$, the equation is called homogeneous linear; otherwise it is non-homogeneous.

Equations containing terms like $y^2$, $\sin y$, or $y,y'$ are nonlinear.

Initial Conditions and Boundary Conditions

  • Initial value problem (IVP): given $y(x_0) = y_0$ (first order), or $y(x_0) = y_0,; y'(x_0) = y_1$ (second order), the solution is uniquely determined.
  • Boundary value problem (BVP): conditions are specified at two different points, e.g. $y(0) = 0,; y(\pi) = 0$.

The Existence and Uniqueness Theorem (Picard–Lindelöf)

Theorem (first-order case). Let $f(x, y)$ be continuous on the rectangle $R = {(x,y) : |x - x_0| \leq a,; |y - y_0| \leq b}$ and satisfy the Lipschitz condition in $y$:

$$|f(x, y_1) - f(x, y_2)| \leq L, |y_1 - y_2|$$

Then the initial value problem $y' = f(x,y),; y(x_0) = y_0$ has a unique solution on $|x - x_0| \leq h$ ($h = \min(a,, b/M)$, $M = \max_R |f|$).

Generalization to order $n$. Convert the $n$-th order equation into a first-order system $\mathbf{Y}' = \mathbf{F}(x, \mathbf{Y})$ and impose the same Lipschitz condition on $\mathbf{F}$.

This theorem guarantees that when the right-hand side is sufficiently "nice", the solution of an ODE exists and is unique. It is the theoretical foundation of all symbolic solvers — if a symbolic method finds a solution, Picard–Lindelöf guarantees it is the unique solution.


Core Theory

First-Order ODEs

Separable Equations

Form:

$$g(y), y' = f(x)$$

Solution steps:

  1. Write the equation as $g(y), dy = f(x), dx$
  2. Integrate both sides: $\displaystyle\int g(y), dy = \int f(x), dx + C$

Example. Solve $y' = xy$:

$$\frac{dy}{y} = x, dx \quad\Longrightarrow\quad \ln|y| = \frac{x^2}{2} + C \quad\Longrightarrow\quad y = C_1, e^{x^2/2}$$

Implementation in oCAS. The classifier is_separable decides separability by checking whether the equation can be written so that "all terms containing $y$ are paired with $y'$, and the remaining terms contain only $x$". The solver solve_separable splits the equation into a $(y\text{-part},, x\text{-part})$ pair, integrates each part, and constructs the implicit solution $\int g, dy - \int f, dx = C$.


Linear First-Order Equations

Form:

$$y' + p(x), y = q(x)$$

Integrating factor method. Multiply by the integrating factor

$$\mu(x) = e^{\int p(x), dx}$$

The equation becomes

$$\frac{d}{dx}!\bigl[\mu(x), y\bigr] = \mu(x), q(x)$$

Integrating gives

$$y = \frac{1}{\mu(x)} \int \mu(x), q(x), dx + \frac{C}{\mu(x)}$$

Derivation. Observe that $\mu' = p,\mu$, hence

$$\mu, y' + p,\mu, y = \mu, y' + \mu', y = (\mu, y)'$$

Example. Solve $y' - \dfrac{2}{x}, y = x^2$:

$$p(x) = -\frac{2}{x}, \quad \mu = e^{\int -2/x, dx} = e^{-2\ln x} = x^{-2}$$

$$\frac{d}{dx}!\bigl[x^{-2} y\bigr] = x^{-2} \cdot x^2 = 1$$

$$x^{-2}, y = x + C \quad\Longrightarrow\quad y = x^3 + Cx^2$$

Implementation in oCAS. solve_linear_first extracts $p(x)$ and $q(x)$, computes $\mu = \exp!\bigl(\int p, dx\bigr)$, and returns

$$y = \mu^{-1}!\left(\int \mu, q, dx + C\right)$$


Bernoulli Equations

Form:

$$y' + p(x), y = q(x), y^n, \qquad n \neq 0,, 1$$

Transformation. Let $v = y^{1-n}$; then $v' = (1-n), y^{-n}, y'$. Multiplying both sides of the original equation by $(1-n), y^{-n}$:

$$(1-n), y^{-n}, y' + (1-n), p(x), y^{1-n} = (1-n), q(x)$$

i.e.

$$v' + (1-n), p(x), v = (1-n), q(x)$$

This is a linear first-order equation in $v$; solve it with the integrating factor method and substitute back $v = y^{1-n}$.

Example. Solve $y' + \dfrac{1}{x}, y = x, y^2$ ($n = 2$):

Let $v = y^{-1}$, so $v' = -y^{-2}, y'$, giving

$$-v' + \frac{1}{x}, v = x \quad\Longrightarrow\quad v' - \frac{1}{x}, v = -x$$

With the integrating factor $\mu = x^{-1}$, we get $v = -x^2 + Cx$, i.e. $y = \dfrac{1}{Cx - x^2}$.

Implementation in oCAS. solve_bernoulli detects the nonlinear power $n$, constructs the substitution $v = y^{1-n}$, calls solve_linear_first on the linearized equation, and then substitutes back.


Exact Equations

Form:

$$M(x,y) + N(x,y), y' = 0$$

If there exists a potential function $F(x,y)$ with $\dfrac{\partial F}{\partial x} = M$ and $\dfrac{\partial F}{\partial y} = N$, then the equation is equivalent to $\dfrac{d}{dx} F(x, y(x)) = 0$, and the solution is $F(x,y) = C$.

Exactness criterion. The equation is exact if and only if

$$\frac{\partial M}{\partial y} = \frac{\partial N}{\partial x}$$

Solution steps:

  1. Integrate $\partial F/\partial x = M$ with respect to $x$: $F = \int M, dx + h(y)$
  2. Determine $h(y)$ from $\partial F/\partial y = N$: $h'(y) = N - \partial/\partial y \int M, dx$
  3. Substitute to obtain $F(x,y) = C$

Integrating factors for non-exact equations. If the equation is not exact, one may try to find an integrating factor $\mu$:

  • if $\dfrac{M_y - N_x}{N}$ depends only on $x$, then $\mu(x) = \exp!\left(\int \dfrac{M_y - N_x}{N}, dx\right)$
  • if $\dfrac{N_x - M_y}{M}$ depends only on $y$, then $\mu(y) = \exp!\left(\int \dfrac{N_x - M_y}{M}, dy\right)$

Example. Solve $(2xy + 3) + (x^2 + 4y), y' = 0$:

$M_y = 2x = N_x$, so the equation is exact.

$$F = \int (2xy + 3), dx = x^2 y + 3x + h(y)$$

$$F_y = x^2 + h'(y) = x^2 + 4y \quad\Longrightarrow\quad h'(y) = 4y \quad\Longrightarrow\quad h = 2y^2$$

Solution: $x^2 y + 3x + 2y^2 = C$.

Implementation in oCAS. solve_exact first verifies exactness with partials_equal. If the equation is not exact, find_integrating_factor tries the two integrating factors above (the cases depending only on $x$ or only on $y$). Once the potential function is found, an implicit solution is returned.


Homogeneous Equations

Form:

$$y' = f!\left(\frac{y}{x}\right)$$

Equivalently, the right-hand side $f(x,y)$ is a degree-zero homogeneous function of $(x,y)$: $f(tx, ty) = f(x,y)$.

Transformation. Let $v = y/x$, so $y = vx$ and $y' = v + xv'$. Substituting:

$$v + x, v' = f(v) \quad\Longrightarrow\quad x, v' = f(v) - v$$

This is a separable equation in $v$ and $x$:

$$\frac{dv}{f(v) - v} = \frac{dx}{x}$$

Integrate and substitute back $v = y/x$.

Example. Solve $y' = \dfrac{y + x}{x} = \dfrac{y}{x} + 1$:

Let $v = y/x$; then $v + xv' = v + 1$, so $xv' = 1$, giving $v = \ln|x| + C$ and $y = x\ln|x| + Cx$.

Implementation in oCAS. The classifier is_homogeneous requires the equation to be first-order linear, with all additive terms having the same total degree in $(x, y)$ (degree-zero homogeneity), and with a term free of $y$ present. The solver solve_homogeneous performs the substitution $v = y/x$, reducing the equation to a separable one.


Second-Order Linear ODEs

The general form of a second-order linear ODE is

$$a(x), y'' + b(x), y' + c(x), y = f(x)$$

Superposition principle. If $y_1, y_2$ are two linearly independent solutions of the homogeneous equation and $y_p$ is a particular solution of the non-homogeneous equation, then the general solution is

$$y = C_1, y_1 + C_2, y_2 + y_p$$

Constant Coefficient Equations

Form:

$$a, y'' + b, y' + c, y = f(x), \qquad a, b, c \in \mathbb{R},; a \neq 0$$

Characteristic equation. The characteristic equation of the homogeneous equation $ay'' + by' + cy = 0$ is

$$a, r^2 + b, r + c = 0$$

The discriminant $\Delta = b^2 - 4ac$ determines the form of the solution:

DiscriminantCharacteristic rootsGeneral homogeneous solution
$\Delta > 0$$r_1, r_2 \in \mathbb{R},; r_1 \neq r_2$$C_1, e^{r_1 x} + C_2, e^{r_2 x}$
$\Delta = 0$$r_1 = r_2 = r$$(C_1 + C_2, x), e^{rx}$
$\Delta < 0$$\alpha \pm \beta i$$e^{\alpha x}(C_1 \cos\beta x + C_2 \sin\beta x)$

The method of undetermined coefficients. When $f(x)$ is a polynomial, an exponential $e^{kx}$, a trigonometric function $\sin\omega x$, $\cos\omega x$, or a product/sum of these, the form of the particular solution can be guessed.

Form of $f(x)$Guessed form of $y_p$
$P_n(x)$ (a polynomial of degree $n$)$Q_n(x) = a_0 + a_1 x + \cdots + a_n x^n$
$F, e^{kx}$$A, e^{kx}$
$f_c \cos\omega x + f_s \sin\omega x$$A\cos\omega x + B\sin\omega x$
$F, e^{kx} \cos\omega x$$e^{kx}(A\cos\omega x + B\sin\omega x)$

Resonance. When the guessed form is linearly dependent with the homogeneous solutions, multiply by $x^s$ ($s$ the multiplicity of $k$ as a characteristic root):

  • if $k$ is a simple characteristic root: $y_p = Ax, e^{kx}$
  • if $k$ is a double characteristic root: $y_p = Ax^2, e^{kx}$

For polynomial forcing, if $r = 0$ is a characteristic root of multiplicity $s$, the guessed form must be multiplied by $x^s$.

Example. Solve $y'' - 3y' + 2y = e^{3x}$:

The characteristic equation is $r^2 - 3r + 2 = 0$, with roots $r_1 = 1, r_2 = 2$.

General homogeneous solution: $C_1 e^x + C_2 e^{2x}$.

$k = 3$ is not a characteristic root, so guess $y_p = Ae^{3x}$. Substituting: $9A - 9A + 2A = 1$, so $A = 1/2$.

General solution: $y = C_1 e^x + C_2 e^{2x} + \dfrac{1}{2} e^{3x}$.

Implementation in oCAS. solve_constant_coeff extracts the coefficients $(a, b, c)$ and the forcing term $f(x)$. constant_coeff_basis constructs the homogeneous basis solutions according to the discriminant. particular_solution_undetermined handles polynomial, exponential, and trigonometric forcing, including complete resonance detection.


Cauchy–Euler Equations

Form:

$$a, x^2, y'' + b, x, y' + c, y = f(x)$$

The coefficient of $y^{(k)}$ is a constant multiple of $x^k$.

Transformation. Let $x = e^t$ (i.e. $t = \ln x$), using

$$y' = \frac{1}{x}, \frac{dy}{dt}, \qquad y'' = \frac{1}{x^2}!\left(\frac{d^2 y}{dt^2} - \frac{dy}{dt}\right)$$

The original equation becomes a constant coefficient equation

$$a, \ddot{y} + (b - a), \dot{y} + c, y = f(e^t)$$

where $\dot{y} = dy/dt$.

Indicial equation. For the homogeneous equation, guess $y = x^r$ directly and substitute to obtain the indicial equation

$$a, r(r-1) + b, r + c = 0 \quad\Longleftrightarrow\quad a, r^2 + (b - a), r + c = 0$$

The structure of the solutions is analogous to the constant coefficient case (three cases according to the discriminant), but the fundamental solutions are $x^r$ instead of $e^{rx}$:

DiscriminantFundamental solutions
$\Delta > 0$$x^{r_1},; x^{r_2}$
$\Delta = 0$$x^r,; x^r \ln x$
$\Delta < 0$ ($\alpha \pm \beta i$)$x^\alpha \cos(\beta\ln x),; x^\alpha \sin(\beta\ln x)$

Example. Solve $x^2 y'' - 2xy' + 2y = 0$:

Indicial equation: $r(r-1) - 2r + 2 = r^2 - 3r + 2 = 0$, with roots $r_1 = 1, r_2 = 2$.

General solution: $y = C_1 x + C_2 x^2$.

Implementation in oCAS. The classifier is_cauchy_euler checks whether the equation matches the pattern $c_k x^k y^{(k)}$. solve_cauchy_euler extracts the Cauchy–Euler coefficients, solves the indicial equation, and constructs the general solution according to the discriminant. cauchy_euler_basis handles the three discriminant cases.


Reduction of Order

When one solution $y_1$ of the homogeneous equation is known, a second linearly independent solution can be obtained by reduction of order.

Method. Let $y_2 = v(x), y_1$ and substitute into $y'' + p, y' + q, y = 0$. Using the fact that $y_1$ satisfies the homogeneous equation, the equation simplifies to a first-order equation in $v'$:

$$v'' y_1 + v'(2y_1' + p, y_1) = 0$$

Let $w = v'$ and separate variables:

$$\frac{w'}{w} = -\frac{2y_1' + p, y_1}{y_1} = -2, \frac{y_1'}{y_1} - p$$

Integrating gives

$$w = \frac{e^{-\int p, dx}}{y_1^2}$$

Hence

$$y_2 = y_1 \int \frac{e^{-\int p, dx}}{y_1^2}, dx$$

Example. $x^2 y'' - 2xy' + 2y = 0$, with known solution $y_1 = x$.

Standard form: $y'' - \dfrac{2}{x}, y' + \dfrac{2}{x^2}, y = 0$, so $p = -2/x$.

$$y_2 = x \int \frac{e^{\int 2/x, dx}}{x^2}, dx = x \int \frac{x^2}{x^2}, dx = x \cdot x = x^2$$

Implementation in oCAS. solve_reduction_of_order first tries simple candidate solutions ($1, x, x^2, e^x, e^{-x}, e^{2x}$) in the homogeneous equation. Once $y_1$ is found, the formula above computes $y_2$. For non-homogeneous equations, variation of parameters is then used to find a particular solution.


Variation of Parameters

When the method of undetermined coefficients does not apply (e.g. $f(x)$ is not of standard form), variation of parameters is the general method.

Method. Given two linearly independent solutions $y_1, y_2$ of the homogeneous equation, look for a particular solution of the non-homogeneous equation of the form

$$y_p = u_1(x), y_1 + u_2(x), y_2$$

where $u_1, u_2$ satisfy

$$\begin{cases} u_1', y_1 + u_2', y_2 = 0 \ u_1', y_1' + u_2', y_2' = g(x) \end{cases}$$

Here $g(x) = f(x)/a$ is the right-hand side of the standard form. By Cramer's rule:

$$u_1' = -\frac{y_2, g}{W}, \qquad u_2' = \frac{y_1, g}{W}$$

where $W = y_1, y_2' - y_1', y_2$ is the Wronskian.

Therefore

$$y_p = -y_1 \int \frac{y_2, g}{W}, dx + y_2 \int \frac{y_1, g}{W}, dx$$

Significance of the Wronskian. $y_1, y_2$ are linearly independent if and only if $W \neq 0$. For a second-order homogeneous linear ODE, Abel's formula gives $W(x) = W(x_0), \exp!\bigl(-\int_{x_0}^x p, dt\bigr)$, so $W$ is either identically zero or never zero.

Implementation in oCAS. variation_of_parameters takes the two basis solutions $y_1, y_2$ and the standard-form right-hand side $g$, computes the Wronskian, and then uses oCAS's symbolic integrator (integrate) to compute $u_1, u_2$. If the integrals cannot be evaluated in closed form (returning an unevaluated Integral(...)), the method is abandoned.


Series Solutions

When the coefficients of an ODE are not constant and no elementary method applies, series methods provide a systematic way to construct solutions.

Power Series Solutions (Ordinary Points)

Applicability. Consider the second-order linear ODE

$$y'' + p(x), y' + q(x), y = 0$$

If $p(x)$ and $q(x)$ are analytic at $x = x_0$ (i.e. they can be expanded in convergent power series), then $x_0$ is an ordinary point, and the equation has two linearly independent analytic solutions near $x_0$.

Method. Let

$$y = \sum_{n=0}^{\infty} a_n (x - x_0)^n$$

Then

$$y' = \sum_{n=1}^{\infty} n, a_n (x - x_0)^{n-1}, \qquad y'' = \sum_{n=2}^{\infty} n(n-1), a_n (x - x_0)^{n-2}$$

Substitute the power series of $y, y', y''$ and of $p(x), q(x)$ into the equation, collect like powers, and set the coefficient of each power to zero, obtaining a recurrence relation for the $a_n$.

For a $k$-th order ODE, the initial coefficients $a_0, a_1, \dots, a_{k-1}$ are free parameters (corresponding to the $k$ arbitrary constants of the general solution); the remaining coefficients are determined by the recurrence.

Example. Solve $y'' - xy = 0$ (the Airy equation) by a series about $x = 0$:

$$\sum_{n=2}^{\infty} n(n-1)a_n x^{n-2} - x \sum_{n=0}^{\infty} a_n x^n = 0$$

$$\sum_{n=0}^{\infty} (n+2)(n+1)a_{n+2} x^n - \sum_{n=0}^{\infty} a_n x^{n+1} = 0$$

The $x^0$ term: $2 \cdot 1 \cdot a_2 = 0$, so $a_2 = 0$.

The $x^n$ ($n \geq 1$) term: $(n+2)(n+1), a_{n+2} - a_{n-1} = 0$, i.e.

$$a_{n+2} = \frac{a_{n-1}}{(n+2)(n+1)}$$

Taking $a_0 = 1, a_1 = 0$: $a_3 = a_0/(3 \cdot 2) = 1/6$, $a_6 = a_3/(6 \cdot 5) = 1/180$, …

Taking $a_0 = 0, a_1 = 1$: $a_4 = a_1/(4 \cdot 3) = 1/12$, $a_7 = a_4/(7 \cdot 6) = 1/504$, …

Implementation in oCAS. solve_power_series sets $y = \sum a_n x^n$, constructs the series triple of $y, y', y''$ (build_series_triple), substitutes it into the equation, groups by powers of $x$, and solves the linear equations for the $a_n$ (solve_linear_coeff). It returns the truncated series as ODESolution::Series(expr, n\_terms).


The Frobenius Method (Regular Singular Points)

Applicability. If $x = x_0$ is not an ordinary point, but $(x - x_0), p(x)$ and $(x - x_0)^2, q(x)$ are analytic at $x_0$, then $x_0$ is a regular singular point.

Method. Let

$$y = x^r \sum_{n=0}^{\infty} a_n x^n = \sum_{n=0}^{\infty} a_n x^{n+r}, \qquad a_0 \neq 0$$

Substitute into the equation. The coefficient of the lowest power $x^{r-2}$ (from the Cauchy–Euler-type leading part) gives the indicial equation:

$$A, r(r-1) + B, r + C = 0$$

where $A, B, C$ are determined by the coefficients of the equation at $x_0$. The higher powers give the recurrence relation for the $a_n$.

Relations between the indicial roots. Let the two roots of the indicial equation be $r_1, r_2$ ($r_1 \geq r_2$):

RootsForm of the solutions
$r_1 - r_2 \notin \mathbb{Z}$two independent series solutions $y_1 = x^{r_1}\sum a_n x^n$, $y_2 = x^{r_2}\sum b_n x^n$
$r_1 = r_2$$y_1 = x^{r_1}\sum a_n x^n$, $y_2 = y_1 \ln x + x^{r_1}\sum c_n x^n$
$r_1 - r_2 \in \mathbb{Z}^+$$y_1$ as above; $y_2$ may contain a $\ln x$ term (the logarithmic case)

Example. Solve $x^2 y'' + xy' + (x^2 - 1)y = 0$ (Bessel's equation, $\nu = 1$) near $x = 0$:

$p(x) = 1/x$ and $q(x) = 1 - 1/x^2$. So $x = 0$ is a regular singular point.

Let $y = \sum a_n x^{n+r}$. After substitution, the coefficient of $x^r$ is

$$a_0[r(r-1) + r - 1] = a_0(r^2 - 1) = 0$$

Indicial equation: $r^2 - 1 = 0$, with roots $r_1 = 1, r_2 = -1$.

Since $r_1 - r_2 = 2 \in \mathbb{Z}$, the logarithmic case must be checked.

Implementation in oCAS. solve_frobenius sets $y = x^r \sum a_n x^n$ ($r$ a symbolic variable), substitutes into the equation, and groups by powers of $x$. The lowest-power group is used by indicial_coeffs to extract the integer coefficients of the indicial equation $Ar^2 + Br + C = 0$. After solving for $r$, the higher groups determine the $a_n$ by recurrence. Currently only the case of rational real roots of the indicial equation is handled, returning the series corresponding to the larger root.


The Laplace Transform

The Laplace transform is a powerful tool for solving initial value problems for linear constant-coefficient ODEs, especially with piecewise continuous forcing or impulse functions.

Definition

The Laplace transform of a function $f(x)$ ($x \geq 0$) is defined as

$$\mathcal{L}{f}(s) = F(s) = \int_0^{\infty} e^{-sx}, f(x), dx$$

Transforms of Derivatives

By integration by parts:

$$\mathcal{L}{y'}(s) = s, Y(s) - y(0)$$

$$\mathcal{L}{y''}(s) = s^2, Y(s) - s, y(0) - y'(0)$$

In general

$$\mathcal{L}{y^{(n)}}(s) = s^n, Y(s) - \sum_{k=0}^{n-1} s^{n-1-k}, y^{(k)}(0)$$

Common Transform Pairs

$f(x)$$F(s) = \mathcal{L}{f}(s)$
$1$$\dfrac{1}{s}$
$x^n$$\dfrac{n!}{s^{n+1}}$
$e^{kx}$$\dfrac{1}{s - k}$
$\cos\omega x$$\dfrac{s}{s^2 + \omega^2}$
$\sin\omega x$$\dfrac{\omega}{s^2 + \omega^2}$
$e^{kx}\cos\omega x$$\dfrac{s - k}{(s-k)^2 + \omega^2}$
$e^{kx}\sin\omega x$$\dfrac{\omega}{(s-k)^2 + \omega^2}$
$x, e^{kx}$$\dfrac{1}{(s-k)^2}$

Steps for Solving an IVP

Take the second-order equation $ay'' + by' + cy = f(x)$, $y(0) = y_0$, $y'(0) = y_1$ as an example:

  1. Transform: apply the Laplace transform to both sides of the equation

$$a[s^2 Y - sy_0 - y_1] + b[sY - y_0] + cY = F(s)$$

  1. Solve the algebraic equation:

$$Y(s) = \frac{F(s) + a(sy_0 + y_1) + by_0}{as^2 + bs + c}$$

  1. Partial fraction decomposition: decompose $Y(s)$ into a linear combination of standard transform pairs

  2. Inverse transform: look up the table to obtain $y(x) = \mathcal{L}^{-1}{Y(s)}(x)$

Key techniques for the inverse transform. For rational functions with a quadratic denominator:

  • Distinct real roots $r_1 \neq r_2$: $\dfrac{n_1 s + n_0}{(s-r_1)(s-r_2)} = \dfrac{A}{s-r_1} + \dfrac{B}{s-r_2}$, with $A = \dfrac{n_1 r_1 + n_0}{r_1 - r_2}$

  • Repeated root $r$: $\dfrac{n_1 s + n_0}{(s-r)^2} = \dfrac{n_1}{s-r} + \dfrac{n_1 r + n_0}{(s-r)^2}$

  • Complex conjugate roots $k \pm i\omega$: $\dfrac{n_1 s + n_0}{(s-k)^2 + \omega^2} = e^{kx}!\left[n_1 \cos\omega x + \dfrac{n_0 + kn_1}{\omega}\sin\omega x\right]$

Example. Solve $y'' - 3y' + 2y = 4$, $y(0) = 2$, $y'(0) = 3$:

$$s^2 Y - 2s - 3 - 3(sY - 2) + 2Y = \frac{4}{s}$$

$$(s^2 - 3s + 2)Y = \frac{4}{s} + 2s - 3 = \frac{2s^2 - 3s + 4}{s}$$

$$Y = \frac{2s^2 - 3s + 4}{s(s-1)(s-2)}$$

Partial fractions: $Y = \dfrac{2}{s} - \dfrac{3}{s-1} + \dfrac{3}{s-2}$.

Inverse transform: $y(x) = 2 - 3e^x + 3e^{2x}$.

Verification: $y(0) = 2$, $y'(0) = -3 + 6 = 3$, and $y'' - 3y' + 2y = 4$ ✓.

Implementation in oCAS. dsolve_ivp is the entry point for IVP solving; internally it calls solve_laplace.

The workflow of solve_laplace:

  1. Determine whether the equation is a first- or second-order linear constant-coefficient equation
  2. Extract the coefficients $(a, b, c)$ and the forcing term $f(x)$
  3. laplace_transform computes $\mathcal{L}{f}(s)$ — supporting polynomials, exponentials, trigonometric functions, and their products
  4. Construct $Y(s) = \dfrac{F(s) + \text{initial value terms}}{as^2 + bs + c}$
  5. inverse_laplace decomposes the rational function with split_fraction and calls inverse_distinct_roots, inverse_repeated_root, or inverse_complex_roots on each term
  6. Return ODESolution::Explicit(y(x)) (without arbitrary constants)

2×2 Linear Systems with Constant Coefficients

Consider a first-order linear system with constant coefficients

$$\mathbf{Y}' = A, \mathbf{Y}, \qquad \mathbf{Y} = \begin{pmatrix} y_1 \ y_2 \end{pmatrix}, \quad A = \begin{pmatrix} a_{11} & a_{12} \ a_{21} & a_{22} \end{pmatrix}$$

General Solution: the Matrix Exponential

The formal solution is $\mathbf{Y}(x) = e^{Ax}, \mathbf{Y}(0)$. Practical solving depends on the eigenvalues of $A$.

Eigenvalue Decomposition

The characteristic equation of $A$ is

$$\lambda^2 - (a_{11} + a_{22})\lambda + (a_{11}a_{22} - a_{12}a_{21}) = 0$$

Let the eigenvalues be $\lambda_1, \lambda_2$ with corresponding eigenvectors $\mathbf{v}_1, \mathbf{v}_2$.

Case 1: distinct real eigenvalues ($\lambda_1 \neq \lambda_2 \in \mathbb{R}$)

$$\mathbf{Y} = C_1, e^{\lambda_1 x}, \mathbf{v}_1 + C_2, e^{\lambda_2 x}, \mathbf{v}_2$$

Case 2: repeated eigenvalue ($\lambda_1 = \lambda_2 = \lambda$)

If $A$ is not diagonalizable (only one linearly independent eigenvector $\mathbf{v}$), a generalized eigenvector $\mathbf{w}$ satisfying $(A - \lambda I)\mathbf{w} = \mathbf{v}$ is needed:

$$\mathbf{Y} = C_1, e^{\lambda x}, \mathbf{v} + C_2, e^{\lambda x}(x, \mathbf{v} + \mathbf{w})$$

Case 3: complex conjugate eigenvalues ($\lambda = \alpha \pm \beta i$, $\beta \neq 0$)

The eigenvector also has real and imaginary parts: $\mathbf{v} = \mathbf{p} + i\mathbf{q}$. The real-valued general solution is

$$\mathbf{Y} = e^{\alpha x}\bigl[C_1(\mathbf{p}\cos\beta x - \mathbf{q}\sin\beta x) + C_2(\mathbf{p}\sin\beta x + \mathbf{q}\cos\beta x)\bigr]$$

Example. Solve the harmonic oscillator system $y_1' = y_2,; y_2' = -y_1$:

$$A = \begin{pmatrix} 0 & 1 \ -1 & 0 \end{pmatrix}$$

Characteristic equation: $\lambda^2 + 1 = 0$, so $\lambda = \pm i$ ($\alpha = 0, \beta = 1$).

For $\lambda = i$: $(A - iI)\mathbf{v} = 0$, giving $\mathbf{v} = (1, i)^T$, $\mathbf{p} = (1, 0)^T$, $\mathbf{q} = (0, 1)^T$.

$$\mathbf{Y} = C_1 \begin{pmatrix} \cos x \ -\sin x \end{pmatrix} + C_2 \begin{pmatrix} \sin x \ \cos x \end{pmatrix}$$

i.e. $y_1 = C_1\cos x + C_2\sin x$, $y_2 = -C_1\sin x + C_2\cos x$.

Implementation in oCAS. solve_linear_system handles $2 \times 2$ constant-coefficient systems:

  1. Extract the entries of the matrix $A$ from the system (extract_coeff)
  2. Compute the characteristic polynomial and find the eigenvalues (within the integers or rationals)
  3. For distinct real eigenvalues: eigenvector finds integer eigenvectors
  4. For a repeated eigenvalue: generalized_eigenvector finds the generalized eigenvector $(A - \lambda I)\mathbf{w} = \mathbf{v}$
  5. For complex eigenvalues: solve_complex_2x2 constructs the real-valued fundamental solutions
  6. Return ODESolution::System([y_1, y_2]), containing the arbitrary constants $C_1, C_2$

Implementation in oCAS

The Classifier (classify.rs)

classify_ode is the entry-point decision maker of ODE solving. It analyzes the structure of the equation and returns the list of candidate methods in priority order:

PriorityODETypeDetection condition
1LinearFirstfirst order; $y$ and $y'$ appear linearly, no nonlinear terms like $y^2, \sin y$, and $y$ is present
2Bernoullifirst order; contains a nonlinear power $y^n$ ($n \geq 2$)
3Separablefirst order; the terms containing $y$ can be grouped with $y'$, the remaining terms contain only $x$
4Exactfirst order; $M + Ny' = 0$ with $\partial M/\partial y = \partial N/\partial x$
5Homogeneousfirst-order linear; all additive terms have the same total degree in $(x,y)$ (degree-zero homogeneity), and a term free of $y$ is present
6LinearConstantCoefforder $\geq 2$; linear; the coefficients of the derivatives do not contain the independent variable
7CauchyEulerorder $\geq 2$; the coefficient of $y^{(k)}$ is $c_k x^k$
8ReductionOfOrdersecond-order linear, not of the types above
9PowerSeriesfallback option for linear ODEs (first try a power series at an ordinary point, then Frobenius)

dsolve tries the methods in this order; the first one that succeeds provides the result. The user can also specify a concrete method via the hint parameter.

The Solver Architecture

dsolve(ctx, ode, hint?)
├── normalize_ode()          // simplify the equation
├── classify_ode()           // return the list of candidate methods
└── try in order:
    ├── first_order::solve_linear_first()
    ├── first_order::solve_bernoulli()
    ├── first_order::solve_separable()
    ├── first_order::solve_exact()
    ├── first_order::solve_homogeneous()
    ├── second_order::solve_constant_coeff()
    ├── second_order::solve_cauchy_euler()
    ├── second_order::solve_reduction_of_order()
    └── series::solve_power_series() or series::solve_frobenius()

dsolve_ivp(ctx, ode, y0, y1?)
└── laplace::solve_laplace()    // the Laplace transform method

dsolve_system(ctx, equations, funcs, var)
└── systems::solve_linear_system()  // 2×2 eigenvalue decomposition

Return Value Types

The ODESolution enum covers all possible output forms:

VariantMeaningUse case
Explicit(expr)$y = \text{expr}$most successful solves
Implicit(expr)$F(x,y) = 0$separable equations (implicit solutions)
Parametric(x_t, y_t)$(x(t), y(t))$parametric solutions
Series(expr, n_terms)truncated seriespower series / Frobenius
System([y1, y2])the individual components$2 \times 2$ systems
Unsolved(ode)the original equationall methods failed

Integration with Symbolic Integration

The ODE solvers depend heavily on oCAS's symbolic integration capabilities:

  • First-order linear: the integrating factor $\mu = \exp(\int p, dx)$ and the final integral $\int \mu, q, dx$
  • Bernoulli: the integrating factor of the linearized equation
  • Exact equations: the double integral of the potential function
  • Homogeneous equations: the integral after separation
  • Variation of parameters: $\int y_i, g / W, dx$
  • Reduction of order: $\int e^{-\int p, dx} / y_1^2, dx$

These integrals call oCAS's layered integration pipeline (rational function → Risch → trigonometric → special functions → heuristic); see Symbolic Calculus and The Risch Integration Algorithm.

Limitations

Known limitations of the current implementation:

  1. Orders above two: only linear constant-coefficient and Cauchy–Euler types are supported. General variable-coefficient higher-order equations have no dedicated solver.
  2. Nonlinear ODEs: apart from Bernoulli (which linearizes), nonlinear equations usually return Unsolved.
  3. Laplace transform: only handles constant-coefficient equations with integer coefficients, and the forcing term must have a tabulated transform.
  4. Series solutions: expand 8 terms at $x_0 = 0$ by default. The Frobenius method only handles the case where the indicial equation has rational real roots.
  5. 2×2 systems: only constant coefficients are supported, and the eigenvalues must be integers or rationals.
  6. Verification: verify_solution substitutes the solution back into the original equation and checks that the residual is zero (used only for testing).

References

  1. Boyce, W. E. & DiPrima, R. C. Elementary Differential Equations and Boundary Value Problems, 11th ed., Wiley, 2017. — Chapters 2–7 cover first-order equations (Ch.2), second-order linear (Ch.3–4), series solutions (Ch.5), Laplace transform (Ch.6), and systems (Ch.7).

  2. Coddington, E. A. & Levinson, N. Theory of Ordinary Differential Equations, McGraw-Hill, 1955. — Rigorous treatment of existence, uniqueness, and stability.

  3. Ince, E. L. Ordinary Differential Equations, Dover, 1956. — Classical reference on special functions and series methods.

  4. Zill, D. G. A First Course in Differential Equations with Modeling Applications, 12th ed., Cengage, 2021. — Accessible introduction with applications.

  5. Tenenbaum, M. & Pollard, H. Ordinary Differential Equations, Dover, 1985. — Comprehensive classical methods including exact equations and integrating factors.

The Risch Integration Algorithm

Prerequisites


Basic Concepts

The Precise Definition of Elementary Functions

The term "elementary function" is usually used intuitively in calculus textbooks. The Risch algorithm requires a precise definition:

Definition 1 (Field of constants). Let $k$ be a differential field of characteristic zero with derivation denoted by $D$. The most basic field of constants is $\mathbb{Q}$, on which $D = 0$.

Definition 2 (Elementary extension). Let $k$ be a differential field and $K$ an extension field of $k$. $K/k$ is called an elementary extension if $K$ is generated from $k$ by a finite chain of extensions of the following three types:

  1. Algebraic extension: $K = k(\alpha)$, where $\alpha$ is algebraic over $k$ (i.e. $\alpha$ is a root of some polynomial in $k[x]$). For example $\mathbb{Q}(\sqrt{2})/\mathbb{Q}$.
  2. Logarithmic extension: $K = k(t)$, where $Dt = Du / u$ for some $u \in k^*$, i.e. $t = \log(u)$. Derivation rule: $D(\log u) = Du/u$.
  3. Exponential extension: $K = k(t)$, where $Dt / t = Du$ for some $u \in k$, i.e. $t = \exp(u)$. Derivation rule: $D(\exp u) = \exp(u) \cdot Du$.

Definition 3 (Elementary function). An elementary function over a field $k$ is an element of some elementary extension tower $k = k_0 \subset k_1 \subset \cdots \subset k_n$ in which each $k_{i+1}/k_i$ is one of the three types above.

Theorem (Liouville, 1835). Let $f$ be an elementary function over $k$. Then $f$ has an elementary antiderivative over $k$ if and only if it can be written as

$$f = Dv + \sum_{i=1}^{n} c_i \cdot \frac{Du_i}{u_i}$$

where $v, u_1, \dots, u_n \in k$ and $c_1, \dots, c_n$ are $D$-constants (i.e. $Dc_i = 0$).

The deep significance of this theorem is that if an elementary function has an antiderivative, the only "new" parts of it can be constant multiples of logarithms. This is the theoretical foundation of the Risch algorithm.

Differential Fields

Definition 4 (Differential field). A differential field is a field $k$ equipped with a derivation $D: k \to k$ satisfying:

  • Additivity: $D(a + b) = Da + Db$
  • Leibniz rule: $D(a \cdot b) = a \cdot Db + Da \cdot b$

The kernel of the derivation ${c \in k : Dc = 0}$ is called the field of constants, denoted $C_k$ or $k^D$.

In the context of the Risch algorithm:

  • The base field is $k_0 = \mathbb{Q}(x)$ with $D = d/dx$ (differentiation with respect to the integration variable $x$); its field of constants is $C_{k_0} = \mathbb{Q}$.
  • A logarithmic extension $k(t) = k(\log u)$ has $Dt = Du/u$.
  • An exponential extension $k(t) = k(\exp u)$ has $Dt = t \cdot Du$.

Key fact: in an elementary extension tower the field of constants stays the same: $C_{k_n} = C_{k_0} = \mathbb{Q}$. This property is used implicitly by the algorithm — when we say "rational constant", we mean an element of $\mathbb{Q}$.


Core Theory

Constructing the Differential-Field Tower

Given an integrand $f$ (an elementary function expression), the first step of the Risch algorithm is to construct the differential-field tower in which it "lives".

Algorithm build_tower(f, var):

  1. Collect function symbols: scan the expression tree of $f$ and collect all function applications (log, exp).
  2. Order them: sort by dependency — if $t_2 = \exp(\log(x) + 1)$, then $t_1 = \log(x)$ must come before $t_2$.
  3. Detect algebraic dependence: for each candidate generator $t_i$, check whether it is algebraically dependent over the tower built so far. The conservative strategy is:
    • $\log(c \cdot u)$ and $\log(u)$ (with $c$ a rational constant) → algebraically dependent ($\log(c \cdot u) = \log(u) + \log(c)$)
    • $\exp(u + c)$ and $\exp(u)$ (with $c$ a rational constant) → algebraically dependent ($\exp(u + c) = e^c \cdot \exp(u)$)
    • otherwise reject (conservative strategy: rather reject a true dependence than merge incorrectly)
    • Since 0.28.0 these are merged by exact field identities instead of being rejected (tower/merge.rs): exp(u + c) = e^c·exp(u), exp(−u) = exp(u)⁻¹, log(cu) = log(u) + log(c), log(u^k) = k·log(u), log(exp(u)) = u, exp(log(u)) = u, registering constant generators (log(2), exp(1), D t = 0) when needed. The tower returns the rewritten integrand, and relations that cannot be decided are still declined honestly.
  4. Reject non-integral powers: if $f$ contains non-integral powers such as $\sqrt{x}$ (i.e. algebraic functions), reject (return None).
  5. Compute derivatives: for each generator $t_i$, compute $Dt_i$ (with respect to $D = d/dx$):
    • $t_i = \log(u)$: $Dt_i = Du / u$ (compute $Du$ recursively)
    • $t_i = \exp(u)$: $Dt_i = t_i \cdot Du$

In oCAS, build_tower lives in ocas-calc/src/tower/build.rs. It returns a Tower struct containing the list of generators gens: Vec<GenInfo> (each with kind, atom reference, and derivative) and the field-of-constants information.

Representation of field elements: elements of the tower are represented by numerator/denominator pairs of sparse multivariate polynomials (SparseMultivariatePolynomial<RationalDomain, Lex>):

  • KElem (k-element): { num: Sparse, den: Sparse }, representing an element $p/q$ of $k_\ell$
  • KPoly (k-polynomial): a univariate polynomial in the top-level generator $t_\ell$ with coefficients in KElem
  • KRat (k-rational function): { num: KPoly, den: KPoly }, representing a rational function in $k_\ell(t_\ell)$

Example. For $f = x \cdot \exp(x) + \log(x)$:

LevelFieldGeneratorDerivation
$k_0$$\mathbb{Q}(x)$$x$$Dx = 1$
$k_1$$k_0(t_1)$, $t_1 = \log(x)$$t_1$$Dt_1 = 1/x$
$k_2$$k_1(t_2)$, $t_2 = \exp(x)$$t_2$$Dt_2 = t_2$

Level-by-Level Integration: The Overall Framework

The core of the Risch algorithm is top-down recursion through the levels. At each level $\ell$ of the tower, the integrand $f$ is viewed as a rational function in $k_{\ell-1}(t_\ell)$.

Algorithm integrate_level(tower, level, f):

Write $f = a/d \in k_\ell(t_\ell)$ with $a, d \in k_\ell[t_\ell]$ and $\gcd(a, d) = 1$.

  1. Hermite reduction (split off the rational part): decompose $a/d$ as

$$\frac{a}{d} = Dg + \frac{a_1}{d_1}$$

where $g \in k_\ell(t_\ell)$ has an explicit antiderivative and $d_1$ is square-free.

  1. Integrate the polynomial part: if the polynomial part of $a/d$ is $p(t_\ell) \in k_\ell[t_\ell]$, integrate it according to the type of the generator:

    • Primitive level ($t_\ell = \log u$): the method of undetermined coefficients
    • Hyperexponential level ($t_\ell = \exp u$): the Risch differential equation
  2. Integrate the logarithmic part: for $a_1/d_1$ ($d_1$ square-free), first check the logarithmic derivative identity (the fast path), and when it misses, run the Rothstein–Trager resultant method (since 0.29.0, rational roots only; irrational and non-constant roots decline honestly).

  3. Base field $\mathbb{Q}(x)$: delegate to the rational-function integrator.

The results are assembled into LevelResult { elem, logs, extras }, where elem is the field-element part, logs is the logarithmic part of the form $\sum c_i \log(v_i)$, and extras is the part that could not be integrated.

New in 0.29.0 (Laurent split at hyperexponential levels): at a level $t_\ell = \exp u$, $t$ divides its own derivative ($Dt = Du \cdot t$) — $t$ is a special polynomial, and a denominator with a $t$-power factor must not go through Hermite reduction (the $\gcd(v, Dv) = 1$ precondition fails; 0.28.0's tower check caught a latent negative-power coefficient mis-scaling this way). The denominator is now split as $t^k \cdot d'$ with $\gcd(d', t) = 1$; the $t^k$ part is integrated layer by layer through Risch differential equations, and only $d'$ reaches Hermite.

Hermite Reduction

Goal: decompose a rational function $a/d$ into an "already integrated" part plus a "simpler" remainder (the denominator of the remainder is square-free).

Algorithm (Hermite reduction as in Bronstein ch. 2; oCAS uses the square-free-factorization recursive form):

Let $a/d$ be a proper fraction ($\deg a < \deg d$) with $d$ monic.

  1. Compute the square-free factorization $d = \prod_j v_j^{m_j}$. If the maximum multiplicity is $m \leq 1$, then $d$ is already square-free; return $(0, a, d)$.
  2. Pick a factor $v$ of maximum multiplicity ($= m \geq 2$) and write $d = u \cdot v^m$.
  3. Let $B = u \cdot Dv$ (the derivative under the tower derivation; over the base field this is $u \cdot v'$). Use the extended Euclidean algorithm to find $s, t$ with $s \cdot B + t \cdot v = a$ ($s$ reduced modulo $v$).
  4. Recurrence formula: $$\frac{a}{u, v^m} = D!\left(\frac{-s}{(m-1), v^{m-1}}\right) + \frac{t + u \cdot D\bigl(s/(m-1)\bigr)}{u, v^{m-1}}$$
  5. Recurse on the remainder $\dfrac{t + u \cdot D(s/(m-1))}{u, v^{m-1}}$ until the denominator is square-free.

Here $D!\left(\frac{-s}{(m-1) v^{m-1}}\right)$ has the explicit antiderivative $\frac{-s}{(m-1) v^{m-1}}$ itself, and the denominator $d_1$ of the final remainder $a_1/d_1$ is square-free. Each iteration lowers the maximum multiplicity by one, so at most $m$ iterations are needed.

In oCAS, Hermite reduction is implemented in two places:

  • ocas-calc/src/integral/rational.rs: Hermite reduction over the base field $\mathbb{Q}(x)$ (hermite_reduce)
  • ocas-calc/src/integral/risch.rs: Hermite reduction at an arbitrary tower level (hermite_tower)

Example. $\int \frac{1}{(x+1)^2}, dx$:

$d = (x+1)^2$; its square-free factorization is $(x+1)^2$, so the maximum multiplicity is $m = 2$, $v = x+1$, and $u = 1$. Then $B = u \cdot Dv = 1$, and solving $s \cdot B + t \cdot v = 1$ gives $s = 1$ (mod $v$), $t = 0$. The recurrence yields:

$$\frac{1}{(x+1)^2} = D!\left(\frac{-1}{x+1}\right) + \frac{0}{x+1}$$

i.e. $\int = -1/(x+1)$. More generally, $\int a/(x+1)^2$ can always be handled by Hermite reduction.

The Logarithmic Derivative Identity

Core idea: a term of the form $c \cdot \frac{Du}{u}$ has the trivial antiderivative $c \cdot \log(u)$.

Theorem (logarithmic derivative identity). Let $f \in k_\ell(t_\ell)$ and let $d_1$ be square-free. If

$$\frac{a_1}{d_1} = c \cdot \frac{D d_1}{d_1}$$

for some $D$-constant $c$, then

$$\int \frac{a_1}{d_1} = c \cdot \log(d_1)$$

More generally, for a square-free denominator $d_1$, all contributions to the logarithmic part have the form $\sum_i c_i \log(v_i)$, where the $v_i$ are given by the irreducible factors of $d_1$.

How to check it: over the base field $\mathbb{Q}(x)$, check whether $a_1$ equals some rational constant $c$ times $d_1'$. At higher tower levels, check whether $a_1$ is a constant of the coefficient field times $d_1'$.

A more precise method (Rothstein–Trager resultant): for a general square-free denominator $d(x)$, the logarithmic part of $\int a(x)/d(x),dx$ is given by the Rothstein–Trager resultant:

$$R(t) = \text{Res}_x!\bigl(d(x),; a(x) - t \cdot d'(x)\bigr)$$

If the roots of $R(t)$ over $\mathbb{Q}$ are $c_1, \dots, c_m$, then

$$\int \frac{a}{d},dx = \sum_{i=1}^{m} c_i \cdot \log\gcd(d,; a - c_i \cdot d') + C$$

In the oCAS implementation (rothstein_trager), $R(t)$ is computed by interpolation — evaluate the resultant at $t = 0, 1, 2, \dots$ and then recover the polynomial by Lagrange interpolation. When $R(t)$ does not split completely over $\mathbb{Q}$, the corresponding terms are returned in the unevaluated form Integral(term, var).

The special case of a quadratic denominator: when $d(x) = x^2 + bx + c$ (irreducible), completing the square gives:

$$\int \frac{Ax + B}{x^2 + bx + c},dx = \frac{A}{2}\log(x^2 + bx + c) + \frac{2B - Ab}{\sqrt{4c - b^2}}\arctan!\left(\frac{2x + b}{\sqrt{4c - b^2}}\right)$$

When $4c - b^2 < 0$ (real-root case), $\arctan$ becomes $\text{artanh}$ (inverse hyperbolic tangent), corresponding to the logarithmic form.

The Risch Differential Equation

The central subproblem of the Risch algorithm is solving the Risch differential equation (RDE).

Problem (the complete fragment since 0.29.0). Given elements $f, g$ of the differential field $k_\ell$ (either may contain the top-level variable $t_\ell$), find $q \in k_\ell(t_\ell)$ (a rational function, no longer restricted to polynomials) satisfying:

$$Dq + f \cdot q = g$$

where $D$ is the tower derivation (total derivative with respect to $t_\ell$).

The solving pipeline (ported function-by-function against SymPy 1.14's integrals/rde.py, itself a mirror of Bronstein Ch. 6):

  1. Weak normalization (Thm 6.1.1): absorb every positive-integer residue of $f$ into a logarithmic derivative, giving $f' = f - Dq_{wn}/q_{wn}$; the original equation is equivalent to $Dz + f'z = q_{wn},g$ with $z = q_{wn},y$.
  2. Normal denominator bound (Thm 6.1.2): compute $h_n$ so that $q_2 = z \cdot h_n$ is polynomial at every normal irreducible, reducing the equation to $a \cdot Dq_2 + b \cdot q_2 = c$.
  3. Special denominator bound (Thm 6.2.1): at hyperexponential levels handle the powers of $t$ (the special polynomial): $r = q_2 \cdot t^\nu \in k[t]$, where $\nu$ follows from the $t$-adic orders of $b$ and $c$; when $\nu_b = 0$ the parametric logarithmic derivative refinement sharpens $\nu$ exactly (implemented over the base field $\mathbb{Q}(x)$ since 0.29.0).
  4. Degree bound (§6.3): an upper bound $n$ on $\deg r$ (the limited_integrate refinements are not ported — they can only lower the bound, and a missed solution becomes an honest decline, never a wrong answer).
  5. SPDE (Rothstein's special polynomial differential equation): reduce any $a \neq 1$ equation round by round to one with constant $a$, $Dh + Bh = C$.
  6. Polynomial-RDE dispatch: no_cancel_b_large (leading-term peel when no cancellation is possible); the cancellation cases go through cancel_exp / cancel_primitive, recursing one level down into the full RDE — which is where rational solutions at lower levels get used (q = 1/t is produced exactly this way).

RDE over the Base Field $\mathbb{Q}(x)$

Over the base field $k_0 = \mathbb{Q}(x)$, the RDE becomes the ordinary differential equation $q'(x) + f(x) \cdot q(x) = g(x)$.

Algorithm (base_rde):

  1. Degree bound: let $\deg f = p$ and $\deg g = r$. If $f = 0$, integrate directly, giving $\deg q = r + 1$; if $f$ is a nonzero constant ($p = 0$), then $\deg q = r$; if $p \geq 1$, the leading term of $f \cdot q$ (of degree $p + \deg q$) dominates $\deg(q') \leq \deg q - 1$, so $\deg q = r - p$ (uniquely determined).
  2. Undetermined coefficients: write $q = \sum_{i=0}^{n} a_i x^i$ and substitute into the equation.
  3. Eliminate from high to low degree: compare the coefficients of the powers of $x$ and determine the $a_i$ one by one. Each $a_i$ is either determined uniquely or contradictory (the antiderivative does not exist).

Example. Solve $q' + q = x$ ($f = 1, g = x$):

Write $q = a_1 x + a_0$. Then $q' = a_1$. Substituting: $a_1 + a_1 x + a_0 = x$.

Comparing coefficients: $x^1: a_1 = 1$; $x^0: a_1 + a_0 = 0 \Rightarrow a_0 = -1$.

Solution: $q = x - 1$. Verification: $(x-1)' + (x-1) = 1 + x - 1 = x$ ✓

This gives $\int x \cdot e^x,dx = (x - 1) e^x$ (at the hyperexponential level).

RDE at a Primitive Level

At a level with $t_\ell = \log(u)$, the derivation satisfies $Dt_\ell = Du/u \in k_{\ell-1}$ (it does not contain $t_\ell$).

Property: $D(a_0 + a_1 t + \cdots + a_m t^m) = Da_0 + Da_1 \cdot t + \cdots + Da_m \cdot t^m + (a_1 + 2a_2 t + \cdots + m a_m t^{m-1}) \cdot Dt$

Since $Dt$ does not contain $t$ (the key property of a primitive extension), applying $D$ to $k[t]$ does not change the degree in $t$ (except that the $\partial/\partial t$ part lowers the degree by one).

Algorithm: start from the top coefficient $a_m$ of $q$ and eliminate top-down:

  1. Determine $a_m$ from the coefficient of $t^m$ in the equation (an RDE for $a_m$ in $k_{\ell-1}$).
  2. Substitute and lower the degree; repeat for $a_{m-1}$.
  3. Finally $a_0$ must satisfy a logarithmic constraint: the constant part of $a_0$ must make the logarithmic terms arising in the lower-level integration consistent.

Recursive structure: each step produces an RDE over $k_{\ell-1}$; call rde_solve recursively down to the base field.

RDE at a Hyperexponential Level

At a level with $t_\ell = \exp(u)$, the derivation satisfies $Dt_\ell = t_\ell \cdot Du$.

Key property: $Dt = t \cdot Du$ contains $t$, so applying $D$ to $k[t]$ mixes degrees:

$$D(a_i t^i) = (Da_i) t^i + a_i \cdot i \cdot t^i \cdot Du = (Da_i + i \cdot a_i \cdot Du) t^i$$

i.e. $D(a_i t^i) = (Da_i + i \cdot a_i \cdot Du) \cdot t^i$ — each power $t^i$ is independent!

Decoupling property: substituting $q = \sum a_i t^i$ and $g = \sum b_j t^j$ into $Dq + fq = g$ and comparing the coefficient of $t^k$:

$$Da_k + k \cdot a_k \cdot Du + f \cdot a_k = b_k$$

i.e.

$$Da_k + (f + k \cdot Du) \cdot a_k = b_k$$

This is an RDE over $k_{\ell-1}$ (unknown $a_k$), and the equations for different $k$ are independent!

Algorithm: for each $k = 0, 1, \dots, \deg g$, solve the RDE

$$Da_k + (f + k \cdot Du) \cdot a_k = b_k$$

independently. If some $a_k$ has no solution, the whole equation has no solution. An upper bound for $\deg q$ is given by $\deg g$.

Example. $\int x \cdot e^x,dx$: on the tower with $t = e^x$ ($Dt = t$), the integrand is $x \cdot t$.

The polynomial part is $p(t) = x \cdot t$ ($m = 1$). Comparing the $t^1$ coefficient: $Da_1 + a_1 \cdot 1 = x$, i.e. $a_1' + a_1 = x$. The base-field RDE gives $a_1 = x - 1$. The $t^0$ part is 0.

Result: $\int = (x-1) \cdot t = (x-1) \cdot e^x$.

The Method of Undetermined Coefficients at Logarithmic Levels

When integrating a polynomial $p(t) = \sum a_i t^i$ at a primitive (logarithmic) level $t = \log(u)$, the method of undetermined coefficients is used.

Algorithm integrate_kpoly_primitive:

Let the polynomial to be integrated be $p(t) = \sum_{i=0}^{m} a_i t^i$ and guess the antiderivative $q(t) = \sum_{i=0}^{m'} b_i t^i$.

  1. Set $m' = m + 1$ (the degree of the antiderivative is at most one higher than that of the integrand, because the $\partial/\partial t$ part of $D$ lowers the degree).
  2. Compute $Dq = \sum (Db_i) t^i + \sum i \cdot b_i \cdot (Du/u) \cdot t^{i-1}$.
  3. Compare the coefficients of $p = Dq$:
    • $t^m$: $a_m = Db_m + (m+1) b_{m+1} \cdot Du/u$
    • $\vdots$
    • $t^0$: $a_0 = Db_0 + b_1 \cdot Du/u$
  4. Solve downwards starting from $t^{m+1}$. Each $b_i$ may require a recursive call to the integrator (for the $Db_i$ part).
  5. The determination of $b_0$ is constrained by the logarithmic constraint: in the final result, all logarithmic terms $\sum c_j \log(v_j)$ must be consistent. If $b_0$ contains logarithmic terms coming from lower levels that do not satisfy the Liouville condition, there is no solution.

Rewriting Trigonometric Functions to Complex Exponentials

The Risch algorithm natively handles only log and exp. Trigonometric functions must first be rewritten in complex-exponential form.

Euler's formula:

$$\sin(u) = \frac{e^{iu} - e^{-iu}}{2i}, \qquad \cos(u) = \frac{e^{iu} + e^{-iu}}{2}$$

$$\tan(u) = \frac{\sin u}{\cos u} = \frac{e^{iu} - e^{-iu}}{i(e^{iu} + e^{-iu})}$$

In oCAS, trig_to_exp (ocas-calc/src/integral/trig.rs) walks the expression tree and replaces sin, cos, tan, cot, sec, csc one by one with the equivalent forms above. The imaginary unit $I$ is added to the tower as a constant generator ($DI = 0$).

Integrating the rewritten form: the rewritten expression lives in the differential-field tower over $\mathbb{Q}(x, I, e^{Ix}, \dots)$ and can be handled by the Risch algorithm.

Conversion back to real form (realify): the result of a Risch integration may contain the imaginary unit. realify attempts to convert the result back into real form:

  • Merging conjugate logarithms: if the result contains $c \cdot \log(u + Iv) + c \cdot \log(u - Iv)$, merge it into $c \cdot \log(u^2 + v^2)$
  • Difference of conjugate logarithms: if it contains $c \cdot \log(u + Iv) - c \cdot \log(u - Iv)$, merge it into $2c \cdot \arctan(v/u)$
  • Merging exponentials: $e^{Iu} \cdot e^{-Iu} = 1$, $e^{Iu} + e^{-Iu} = 2\cos(u)$, etc.

This is a "best-effort" process. If no known pattern matches, the complex form is kept (it is still mathematically correct, since differentiation-verification still holds).

Current limitation: the Risch differential equation solver works over $\mathbb{Q}[x]$, so when trigonometric rewriting produces hyperexponential equations containing $I$ (e.g. the equations arising when integrating $\sin(x)\cos(x)$ or $\cos^2(x)$), the coefficient field extends to $\mathbb{Q}(I)$ rather than $\mathbb{Q}$, and the solver may fail. Such integrands are returned in unevaluated form.

The Heuristic Layer

Before the Risch algorithm, oCAS tries a set of heuristic integration techniques (ocas-calc/src/integral/heuristic.rs). These techniques are fast and cover common patterns.

Integration by Parts (LIATE Ordering)

For a product $f \cdot g$, try integration by parts $\int u,dv = uv - \int v,du$.

LIATE priority (heuristic rule for choosing $u$):

PriorityTypeScoreExample
1Logarithmic0$\log x$, $\arctan x$
2Inverse trig1$\arcsin x$, $\text{arccosh}, x$
3Algebraic2$x^2$, $\sqrt{x}$
4Trigonometric3$\sin x$, $\cos x$
5Exponential4$e^x$, $2^x$

Algorithm: for each factor of the product, sort by LIATE score. Choose the factor with the lowest score (highest priority) as $u$, and the rest (integrated to obtain $v$) as $dv$.

Depth limit: PARTS_MAX_DEPTH = 2, to prevent infinite recursion.

Trigonometric Substitution

Match $\sqrt{a^2 - x^2}$, $\sqrt{a^2 + x^2}$, $\sqrt{x^2 - a^2}$ and their reciprocals, and directly return the known antiderivatives:

IntegrandAntiderivative
$\frac{1}{\sqrt{a^2 - x^2}}$$\arcsin(x/a)$
$\frac{1}{\sqrt{a^2 + x^2}}$$\text{arcsinh}(x/a)$
$\frac{1}{\sqrt{x^2 - a^2}}$$\text{arccosh}(x/a)$
$\sqrt{a^2 - x^2}$$\frac{x\sqrt{a^2-x^2} + a^2 \arcsin(x/a)}{2}$
$\sqrt{a^2 + x^2}$$\frac{x\sqrt{a^2+x^2} + a^2 \text{arcsinh}(x/a)}{2}$
$\sqrt{x^2 - a^2}$$\frac{x\sqrt{x^2-a^2} - a^2 \text{arccosh}(x/a)}{2}$

Weierstrass Substitution

For rational functions of $\sin(u)$ and $\cos(u)$ (with $u$ a linear function of $x$), use the universal substitution $t = \tan(u/2)$:

$$\sin u = \frac{2t}{1+t^2}, \qquad \cos u = \frac{1-t^2}{1+t^2}, \qquad du = \frac{2,dt}{1+t^2}$$

This reduces the integral of a rational function of trigonometric functions to the integral of a rational function in $t$.

Euler Substitution

For integrands containing $\sqrt{ax^2 + bx + c}$, choose the Euler substitution according to the sign of $a$:

  • $a > 0$: $\sqrt{a},x + t = \sqrt{ax^2 + bx + c}$
  • $a < 0$: $t(x - \alpha) = \sqrt{ax^2 + bx + c}$ ($\alpha$ one of the roots)
  • $c > 0$: $\sqrt{c} + xt = \sqrt{ax^2 + bx + c}$

These substitutions reduce integrals of rational functions with square roots to integrals of rational functions.

The Special Function Table

When the Risch algorithm proves that an integral has no elementary antiderivative, many common cases still have closed forms in terms of special functions. oCAS encodes these standard antiderivatives directly in ocas-calc/src/integral/special.rs (definitions consistent with SymPy):

IntegrandAntiderivativeSpecial function
$e^{-x^2}$$\frac{\sqrt{\pi}}{2},\text{erf}(x)$Error function
$e^{x^2}$$\frac{\sqrt{\pi}}{2},\text{erfi}(x)$Imaginary error function
$e^{cx^2}$ ($c < 0$)$\frac{\sqrt{\pi}}{2\sqrt{-c}},\text{erf}(\sqrt{-c},x)$Error function
$e^x / x$$\text{Ei}(x)$Exponential integral
$\sin(x)/x$$\text{Si}(x)$Sine integral
$\cos(x)/x$$\text{Ci}(x)$Cosine integral
$\sinh(x)/x$$\text{Shi}(x)$Hyperbolic sine integral
$\cosh(x)/x$$\text{Chi}(x)$Hyperbolic cosine integral
$\sin(x^2)$$\sqrt{\frac{\pi}{2}},S!\left(\sqrt{\frac{2}{\pi}},x\right)$Fresnel $S$
$\cos(x^2)$$\sqrt{\frac{\pi}{2}},C!\left(\sqrt{\frac{2}{\pi}},x\right)$Fresnel $C$

Matching logic: special_integrate decomposes the integrand into product factors and tries to match the following pattern families:

  • erf family: $e^{c \cdot x^2}$ ($c < 0$), including the special case $e^{-x^2}$
  • Ei family: $e^x / x$, including the generalization $e^{cx}/x$
  • Si/Ci/Shi/Chi family: trigonometric or hyperbolic functions divided by $x$
  • Fresnel family: $\sin(x^2)$, $\cos(x^2)$

Note: these are not part of the Risch algorithm. After Risch proves "no elementary antiderivative exists", the special-function table provides a "suboptimal" answer.


Implementation in oCAS

The Integration Pipeline Architecture

integrate(expr, var) (ocas-calc/src/integral/mod.rs) tries the following layers in order; the first layer that produces an answer wins:

┌────────────────────────────────────────────────────────┐
│  integrate(expr, var)                                  │
├────────────────────────────────────────────────────────┤
│ 1. Lookup table (power rule, linear-argument functions)│
│     ↓ failure                                          │
│  2. Rational-function integrator (integrate_rational)  │
│     ↓ failure                                          │
│  3. Risch algorithm (risch_integrate)                  │
│     ↓ failure                                          │
│  4. Trig rewrite (trig_to_exp) → retry Risch → realify   │
│     ↓ failure                                          │
│  5. Special-function table (special_integrate)         │
│     ↓ failure                                          │
│  6. Heuristic layer (heuristic_integrate)              │
│     ↓ failure                                          │
│  7. Return the unevaluated form Integral(expr, var)    │
└────────────────────────────────────────────────────────┘

Depth limits: MAX_DEPTH = 8 (the recursion depth of integrate_raw) and MAX_RISCH_DEPTH = 16 (the recursion depth of risch_integrate, using a thread_local! counter). These limits prevent stack overflow caused by pathological inputs (e.g. sec(x) triggering infinite VOP retries).

Module Organization

ocas-calc/src/integral/
├── mod.rs         ← integrate entry + lookup tables + pipeline dispatch
├── rational.rs    ← rational-function integrator over the base field Q(x)
│                    Hermite reduction + logarithmic part + Rothstein–Trager
├── risch.rs       ← main loop of the Risch algorithm
│                    per-level integrate_level + Hermite reduction
│                    polynomial part (primitive / hyperexponential)
├── rde.rs         ← Risch differential equation solver
│                    base_rde (Q[x]) + recursive tower
├── trig.rs        ← trigonometric ↔ complex-exponential rewrite (trig_to_exp / realify)
├── heuristic.rs   ← heuristics: parts / trig substitution / Weierstrass / Euler
└── special.rs     ← special-function table (erf, Ei, Si, Ci, Fresnel, etc.)

Execution Flow of the Risch Algorithm

Trace the full execution with $\int (x+1) e^x,dx$ as an example:

  1. Build the tower: build_tower collects $t_1 = \exp(x)$; the tower is $\mathbb{Q}(x) \subset \mathbb{Q}(x)(t_1)$.
  2. risch_integrate: convert the integrand into a rational function in $k_1(t_1)$: $(x+1) \cdot t_1$.
  3. integrate_level(tower, level=1, f):
    • The denominator is 1; no Hermite reduction.
    • Polynomial part $p(t_1) = (x+1) t_1$.
    • Call integrate_kpoly_hyperexp (since $t_1 = \exp(x)$):
      • Compare the $t_1^1$ coefficient: $Da_1 + a_1 \cdot 1 = x + 1$
      • Recursively call rde_solve: $a_1' + a_1 = x + 1$
      • Base-field base_rde: write $a_1 = ax + b$; then $a_1' = a$ and $a + ax + b = x + 1$
      • Compare: $a = 1$, $a + b = 1 \Rightarrow b = 0$
      • $a_1 = x$; verification: $x' + x = 1 + x = x + 1$ ✓
    • The $t_1^0$ coefficient is 0, so $b_0 = 0$.
  4. Assemble the result: $q = x \cdot t_1$, converted to an atom: $x \cdot e^x$.
  5. Simplify + normalize: return $x \cdot e^x$.

Verification: $\frac{d}{dx}[x \cdot e^x] = e^x + x \cdot e^x = (x+1)e^x$ ✓

Internal Representation of Field Elements

In the tower, field elements are represented by flat multivariate sparse polynomials:

#![allow(unused)]
fn main() {
// KElem: an element of k = Q(x, t_1, ..., t_{n-1})
struct KElem {
    num: SparseMultivariatePolynomial<RationalDomain, Lex>,
    den: SparseMultivariatePolynomial<RationalDomain, Lex>,
}
}

Variable index assignment: 0 corresponds to $x$, 1 to $t_1$, …, n-1 to $t_{n-1}$.

KPoly is a dense univariate polynomial in the top-level variable with coefficients in KElem:

#![allow(unused)]
fn main() {
// KPoly: a polynomial in k[t_n] with coefficients in k
struct KPoly {
    coeffs: Vec<KElem>,  // from lowest to highest degree
    top: usize,          // index of the top-level variable
    n: usize,            // total number of variables
}
}

KRat is a rational function $p/q$:

#![allow(unused)]
fn main() {
struct KRat {
    num: KPoly,
    den: KPoly,
}
}

No GCD reduction: KElem does not perform multivariate GCD reduction (because a general multivariate GCD is expensive). Zero detection only checks whether the numerator is zero; cross-multiplied equality is the only reliable equality test.

Fuel-Bounded Integration

integrate_with_fuel(ctx, expr, var, &fuel) threads a Fuel budget through the two post-integration simplification phases. The integration traversal itself uses the MAX_DEPTH / MAX_RISCH_DEPTH limits; Fuel constrains only the simplification phases, preventing pathological results (e.g. integrals producing enormous expressions) from sending the rewriter into an infinite loop.

#![allow(unused)]
fn main() {
use ocas_core::fuel::Fuel;

let fuel = Fuel::new(500);
let result = integrate_with_fuel(&ctx, expr, Symbol::new("x"), &fuel);
// Ok(result)  — completed normally
// Err(_)      — fuel exhausted during simplification
}

Limitations and Fallback Strategy

The current implementation of the Risch algorithm has the following limitations:

LimitationReasonFallback behavior
Only polynomial solutions of the RDE (since 0.29.0: the full rational fragment — weak normalization, normal/special denominator bounds, degree bounds, SPDE, cancellation recursions; the limited_integrate refinements and the higher-level parametric_log_deriv are not implemented)Refinements only lower the degree bound; a miss is an honest declineReturn None; the caller tries other layers
At tower levels the logarithmic part (since 0.29.0: Rothstein–Trager, rational roots only; irrational/non-constant roots decline)Algebraic roots need 0.30.0's algebraic extensionsReturn the unevaluated form
Algebraic functions ($\sqrt{x}$, etc.) not supportedNeeds algebraic-function field extensionsCommon patterns are covered by the heuristic trigonometric substitution
Partial merging of algebraically dependent generators (six exact classes since 0.28.0, plus $\exp(ku) = \exp(u)^k$ since 0.29.0; fractional $\exp(u/k)$ would need re-basing, not implemented)Relation detection is incompleteReturn None
Hyperexponential RDEs containing $I$The RDE solver works only over $\mathbb{Q}[x]$Trigonometric integrands are returned in unevaluated form

When all layers fail, Integral(expr, var) is returned — this is an intentional answer, meaning "this integral has no closed form in the current implementation", not a program error.


References

  1. Bronstein, M. Symbolic Integration I: Transcendental Functions, 2nd ed., Springer, 2005. — The authoritative reference on the Risch algorithm. Chapter 2: integration of rational functions (Hermite reduction, Rothstein–Trager); Chapter 5: the Risch algorithm for elementary functions; Chapter 6: the Risch differential equation. The oCAS implementation follows this book closely.
  2. Bronstein, M. Symbolic Integration II: Transcendental and Algebraic Functions, Springer, 2004. — Extensions to algebraic and special functions. Not yet covered by oCAS.
  3. Geddes, K. O., Czapor, S. R., & Labahn, G. Algorithms for Computer Algebra, Kluwer, 1992. — Chapter 12 offers an alternative presentation of the Risch algorithm.
  4. Liouville, J. "Sur les transcendantes elliptiques de première et de seconde espèce considérées comme fonctions de leur amplitude." Journal de l'École Polytechnique, 1835. — The original source of Liouville's theorem on elementary antiderivatives.
  5. Risch, R. H. "The problem of integration in finite terms." Transactions of the AMS, 139:167–189, 1969. — The founding paper of the Risch algorithm.
  6. Rothstein, M. "A new algorithm for the integration of exponential and logarithmic functions." Proceedings of the 1977 MACSYMA Users Conference, 1977. — The Rothstein–Trager resultant method.
  7. Trager, B. M. "Algebraic factoring and rational function integration." Proceedings of SYMSAC '76, 1976. — Trager's algorithm (factorization over algebraic number fields).
  8. Lazard, D. & Rioboo, R. "Integration of rational functions: Rational computation of the logarithmic part." Journal of Symbolic Computation, 9(2):113–129, 1990. — An efficient algorithm for the logarithmic part.

FGLM & Elimination Theory

This chapter systematically presents the FGLM algorithm for changing the order of Gröbner bases of zero-dimensional ideals, the elimination theorem, and Gröbner-basis-based ideal operations (quotients, saturations, intersections, primary decomposition, radicals). Together these tools form the core infrastructure of oCAS's polynomial-system solving and algebraic-geometry computations.


Prerequisites

Before reading this chapter, we recommend the following background:

  • Gröbner basis theory: Buchberger's algorithm, the F4/F5 algorithms, S-polynomials, reduced Gröbner bases — see Gröbner Basis Theory
  • Polynomial algebra: multivariate polynomial rings $\mathbb{F}[x_1, \ldots, x_n]$, monomial orders (Lex / Grlex / Grevlex) — see Polynomial Algebra
  • Linear algebra: Gaussian elimination, vector-space dimension, linear dependence — see Linear Algebra
  • Polynomial factorization: square-free factorization, univariate polynomial factorization — see Polynomial GCD & Factorization

For a systematic study path, see the Mathematics Overview.


Basic Concepts

Zero-Dimensional Ideals

Definition. Let $I \subseteq R = \mathbb{F}[x_1, \ldots, x_n]$ be an ideal. $I$ is called zero-dimensional if the quotient ring $R/I$ is finite-dimensional as an $\mathbb{F}$-vector space.

Equivalent conditions (any of the following implies zero-dimensionality):

  1. Algebraic geometry: the affine variety $V(I) \subseteq \overline{\mathbb{F}}^n$ is a finite set
  2. Gröbner-basis characterization: for every variable $x_i$ ($1 \leq i \leq n$), the Gröbner basis contains a polynomial $g$ with $\text{lm}(g) = x_i^{N_i}$ (a pure power)
  3. Finiteness of the staircase: the set of standard monomials (monomials not divisible by any leading monomial) is finite

Example. $I = \langle x^2 - 1, y - x \rangle \subseteq \mathbb{Q}[x, y]$. Its Lex Gröbner basis is ${x^2 - 1, y - x}$ with leading terms ${x^2, y}$. For $x$ we have $x^2$ and for $y$ we have $y$ — the pure-power condition is satisfied, so $I$ is zero-dimensional. Indeed $V(I) = {(1, 1), (-1, -1)}$ is finite.

Counterexample. $J = \langle x - y \rangle \subseteq \mathbb{Q}[x, y]$. Its leading terms are ${x}$; there is no pure-power leading term for $y$. $V(J) = {(t, t) : t \in \overline{\mathbb{Q}}}$ is infinite, so $J$ is positive-dimensional.

The Staircase and Standard Monomials

Definition. Let $G = {g_1, \ldots, g_t}$ be a Gröbner basis of an ideal $I$ with respect to a monomial order $\succ$. Let $\text{LT}(I) = \langle \text{lm}(g_1), \ldots, \text{lm}(g_t) \rangle$ be the leading-term ideal.

The set of standard monomials is defined as

$$\text{Std}(I) = {x^\alpha \in \mathbb{F}[x_1, \ldots, x_n] : x^\alpha \notin \text{LT}(I)}$$

i.e. the monomials not divisible by any $\text{lm}(g_i)$. The set of standard monomials is also called the staircase, because in two dimensions its shape resembles a staircase.

Core property. The standard monomials form an $\mathbb{F}$-vector-space basis of $R/I$. That is, every coset $f + I \in R/I$ has a unique representation

$$f + I = \sum_{x^\alpha \in \text{Std}(I)} c_\alpha \cdot x^\alpha + I$$

with $c_\alpha \in \mathbb{F}$. This is equivalent to saying that the normal form of $f$ with respect to the Gröbner basis is a unique linear combination of standard monomials.

Example. $I = \langle x^2, xy \rangle \subseteq \mathbb{F}[x, y]$ (Grevlex order). The leading terms are ${x^2, xy}$.

  • $1$: not divisible by $x^2$ or $xy$ → standard monomial ✓
  • $y$: not divisible → ✓
  • $y^2$: not divisible → ✓
  • $y^k$ ($k \geq 0$): not divisible → ✓
  • $x$: not divisible → ✓
  • $x^2$: divisible by $x^2$ → ✗
  • $xy$: divisible by $xy$ → ✗

The staircase is ${1, x, y, y^2, y^3, \ldots}$ — an infinite set! This reflects that $I$ is positive-dimensional ($V(I) = {(0, t) : t \in \overline{\mathbb{F}}}$).

Dimension. For a zero-dimensional ideal, the size of the staircase

$$D = \dim_{\mathbb{F}}(R/I) = |\text{Std}(I)|$$

is a finite positive integer, called the vector-space dimension of $I$ or the stable value of the Hilbert function. This dimension $D$ plays a central role in the complexity analysis of the FGLM algorithm.

Normal Forms

Definition. The normal form $\text{NF}(f, G)$ of a polynomial $f$ with respect to a Gröbner basis $G$ is the remainder of dividing $f$ by $G$ with remainder. It satisfies:

  1. $\text{NF}(f, G) \in f + I$ ($\text{NF}(f, G)$ is congruent to $f$ modulo $I$)
  2. Every term of $\text{NF}(f, G)$ is a standard monomial
  3. The representation is unique (for a reduced Gröbner basis)

For a zero-dimensional ideal, the normal form can be represented as a coefficient vector of length $D$ — one component per standard monomial. This is precisely the key that lets the FGLM algorithm turn normal-form computation into linear-algebra operations.


Core Theory

The Elimination Theorem

Elimination ideal. Let $I \subseteq \mathbb{F}[x_1, \ldots, x_n]$ and fix $1 \leq \ell \leq n$. The $\ell$-th elimination ideal is

$$I_\ell = I \cap \mathbb{F}[x_{\ell+1}, \ldots, x_n]$$

i.e. the ideal of the polynomials of $I$ that do not involve the first $\ell$ variables.

Elimination Theorem. Let $G$ be a Gröbner basis of $I$ with respect to the Lex order ($x_1 \succ x_2 \succ \cdots \succ x_n$). Then

$$G_\ell = G \cap \mathbb{F}[x_{\ell+1}, \ldots, x_n]$$

is a Gröbner basis of $I_\ell$.

In other words, the polynomials of a Lex Gröbner basis that do not involve $x_1, \ldots, x_\ell$ generate exactly the elimination ideal $I_\ell$.

Sketch of proof. The key property of the Lex order is: if $x^\alpha \succ x^\beta$ and $\alpha_1 = \cdots = \alpha_\ell = 0$, then $\beta_1 = \cdots = \beta_\ell = 0$. Hence the intersection of the leading-term ideal $\langle \text{lt}(g) : g \in G \rangle$ with $\mathbb{F}[x_{\ell+1}, \ldots, x_n]$ is generated exactly by the leading terms of $G_\ell$. $\square$

Applications. The elimination theorem is the theoretical basis of all of the following constructions:

  • Implicit-function elimination: eliminate variables from a system of equations, obtaining relations in the remaining variables only
  • The Rabinowitsch trick for ideal quotients: eliminate after introducing an auxiliary variable
  • Ideal intersection: eliminate after introducing the auxiliary variable $t$
  • Polynomial-system solving: extract constraints variable by variable from the Lex GB

Note. Gröbner bases in the Grevlex order do not have the elimination property. Hence every operation requiring elimination must be performed in the Lex order. This is exactly the core value of the FGLM order-change algorithm: first compute the basis quickly in Grevlex, then convert it to Lex with FGLM for elimination.

The FGLM Algorithm

The FGLM algorithm (Faugère–Gianni–Lazard–Mora, 1993) provides an efficient change of order for Gröbner bases of zero-dimensional ideals. Its core idea: using normal-form computations in the source order, enumerate monomials in the target order and construct the target-order Gröbner basis directly by linear-algebra methods — completely avoiding a re-run of F4 in the target order.

Motivation

Suppose we already have a Gröbner basis $G_{\text{grevlex}}$ of $I$ in the Grevlex order (the most efficient computational order). For elimination or solving, we need a basis $G_{\text{lex}}$ in the Lex order.

Comparison of the two methods:

MethodComplexityDescription
Re-running F4 (Lex)exponential (worst case)recomputes everything; intermediate polynomials may blow up
FGLM$O(n \cdot D^3)$$D = \dim(R/I)$; pure linear algebra

For zero-dimensional ideals, FGLM is almost always faster — especially when $D$ is moderate but the intermediate polynomials of the Lex basis are enormous.

Algorithm Description

Input: a reduced Gröbner basis $G_{\text{src}}$ of a zero-dimensional ideal $I$ in the source order $O_1$, and a target order $O_2$.

Output: a reduced Gröbner basis $G_{\text{tgt}}$ of $I$ in the target order $O_2$.

Initialization:

  1. Extract all leading monomials $\text{lm}(g_1), \ldots, \text{lm}(g_t)$ from $G_{\text{src}}$
  2. Compute the staircase $\text{Std}(I)$ (BFS: start from $1$, extend variable by variable, skip monomials divisible by a leading term, until no new standard monomial appears — zero-dimensionality guarantees termination)
  3. Let $D = |\text{Std}(I)|$ and number the standard monomials $s_1, s_2, \ldots, s_D$
  4. For each variable $x_i$, construct the multiplication matrix $M_i$: for each standard monomial $s_j$, compute the normal form of $x_i \cdot s_j$ (under $G_{\text{src}}$), giving a $D \times D$ matrix

Main loop: enumerate the monomials in increasing target order $O_2$:

seen_nfs ← []     // seen normal forms (D-dimensional vectors)
seen_mons ← []    // corresponding monomials
boundary ← {1}    // boundary set
visited ← ∅       // visited monomials
GB_tgt ← ∅

while boundary ≠ ∅:
    m ← the O2-smallest monomial in boundary
    visited ← visited ∪ {m}

    // compute the normal form of m w.r.t. the source-order basis (D-dim coordinate vector)
    nf ← normal_form(m, G_src, staircase)

    if nf ∈ span(seen_nfs):      // linearly dependent?
        // found a relation nf = Σ c_i · seen_nfs[i]
        coeffs ← Gaussian_solve(seen_nfs, nf)
        // construct the new polynomial: m - Σ c_i · seen_mons[i]
        new_poly ← (m, 1) + Σ (-c_i, seen_mons[i])
        GB_tgt ← GB_tgt ∪ {new_poly}
        // all multiples of m lie in the ideal; mark them as visited
        mark_multiples(visited, m)
    else:
        // linearly independent: m is a new standard monomial in the target order
        seen_nfs.append(nf)
        seen_mons.append(m)
        // add the neighbours of m to the boundary
        for i = 1 to n:
            if m · x_i ∉ visited:
                boundary ← boundary ∪ {m · x_i}

return minimize(GB_tgt).auto_reduce()

Key Subroutines

Normal-form computation. The normal form of a monomial $m$ under $G_{\text{src}}$ is a $D$-dimensional vector. In the implementation, $m$ is turned into the monomial polynomial with coefficient $1$, divided by $G_{\text{src}}$ with remainder, and then each term of the remainder is mapped to its position index in the staircase.

Linear-relation detection. Maintain an augmented matrix $[\text{seen_nfs} \mid \text{nf}]$ ($D$ rows, $k+1$ columns, where $k$ is the number of seen normal forms). Perform Gaussian elimination whenever a new nf is added:

  • if the augmented column is all zero after elimination → linearly dependent; extract the coefficient vector
  • if the augmented column has a non-zero entry after elimination → linearly independent; add to seen_nfs

This can be implemented efficiently with incremental Gaussian elimination (processing one column at a time), avoiding repeated elimination.

Marking multiples. When a linear relation $\text{nf}(m) = \sum c_i \cdot \text{nf}(m_i)$ is found, the constructed polynomial $m - \sum c_i m_i \in I$ has leading term $m$ (in the target order). All multiples $m \cdot x^\alpha$ of $m$ necessarily lie in $I$ as well, so they need not be considered further — they are removed from the boundary.

Complexity Analysis

  • Staircase computation: $O(D)$ monomials, each requiring one divisibility check against every leading monomial of the basis (each check costs $O(n)$ comparisons)
  • Multiplication matrices: $n$ matrices of size $D \times D$, one normal-form computation per entry → $O(n \cdot D^2 \cdot T_{\text{NF}})$
  • Main loop: at most $D$ linearly independent standard monomials are discovered, and each linear-dependence discovery emits one target-basis element; the total number of iterations is $\le D + |G_{\text{tgt}}|$, with the implementation's safety cap at $D(D+1) + 4n$
  • Per iteration: one normal-form computation $O(T_{\text{NF}})$ + one Gaussian elimination $O(D^2)$
  • Total complexity: $O(n \cdot D^3)$ field operations

where $T_{\text{NF}}$ is the cost of one normal-form computation, typically linear in $D$ for sparse polynomials.

Correctness

Theorem (Faugère–Gianni–Lazard–Mora 1993). Let $I$ be a zero-dimensional ideal and $G_{\text{src}}$ a reduced Gröbner basis of $I$ in the order $O_1$. The FGLM algorithm outputs a $G_{\text{tgt}}$ that is a reduced Gröbner basis of $I$ in the order $O_2$.

Sketch of proof:

  1. Normal-form computation is order-independent — the normal-form map w.r.t. $G_{\text{src}}$ and the one w.r.t. $G_{\text{tgt}}$ are representations of the same linear map in different bases
  2. The main loop enumerates monomials in increasing target order and exactly covers all standard monomials and the monomials producing leading terms
  3. The linear-relation detection guarantees that the output polynomials generate exactly the leading-term ideal
  4. minimize and auto_reduce guarantee reducedness

Ideal Operations

Gröbner bases turn the basic arithmetic operations on ideals into algorithms. This section presents the core ideal operations implemented in oCAS; each one relies on the elimination theorem.

Ideal Quotient

Definition. The ideal quotient of two ideals $I, J \subseteq R$ is

$$I : J = {f \in R : f \cdot g \in I, , \forall g \in J}$$

For a principal ideal $J = \langle g \rangle$ it is written simply as $I : g$.

Rabinowitsch trick. The key trick for computing $I : g$ is to introduce a new variable $w$. Note, however, that the auxiliary-variable construction actually computes the saturation:

$$I : g^\infty = \left( I \cdot R[w] + \langle 1 - wg \rangle \right) \cap R$$

i.e. in the extended ring $R[w]$, lift the generators of $I$, add $1 - wg$, compute a Gröbner basis, and then take the polynomials not involving $w$ (elimination). When $I$ is already saturated with respect to $g$ (i.e. $I : g = I : g^\infty$, e.g. square-free ideals without embedded components), this equals $I : g$. oCAS's ideal_quotient implements exactly this elimination construction (mathematically, $I : g^\infty$).

Proof of correctness. By localization, $R[w]/\langle 1 - wg \rangle \cong R[1/g]$ ($w \mapsto 1/g$). Hence $f \in (I \cdot R[w] + \langle 1-wg\rangle) \cap R$ iff $f \in I \cdot R[1/g]$, i.e. iff $g^N f \in I$ for some $N$, i.e. $f \in I : g^N \subseteq I : g^\infty$. Conversely, if $g^N f \in I$, then

$$f = f\bigl(1-(wg)^N\bigr) + w^N \cdot (g^N f)$$

where the first term lies in $\langle 1-wg\rangle$ (since $1-(wg)^N$ is divisible by $1-wg$) and the second in $I \cdot R[w]$. $\square$

Several generators. For $J = \langle g_1, \ldots, g_m \rangle$, one has

$$I : J^\infty = \bigcap_{j=1}^{m} (I : g_j^\infty)$$

Implemented by computing each $I : g_j^\infty$ and then intersecting the results (which is precisely the behavior of ideal_quotient).

Example. $I = \langle x^2, xy \rangle$, $J = \langle x \rangle$.

$$I : x = \langle x^2, xy \rangle : \langle x \rangle$$

In $\mathbb{Q}[x, y, w]$, compute $\text{GB}(x^2, xy, 1 - wx)$ and eliminate $w$:

  • $1 - wx \implies w = 1/x$
  • $x^2 \cdot w = x$, $xy \cdot w = y$ (since $x^2 w - x = -x(1-wx)$ and $xy w - y = -y(1-wx)$)

Elimination yields $\langle x, y \rangle$, hence $\langle x^2, xy \rangle : \langle x \rangle = \langle x, y \rangle$.

Ideal Intersection

Definition. $I \cap J$ is the ideal of the polynomials that belong to both $I$ and $J$.

The auxiliary-variable method. Introduce a new variable $t$:

$$I \cap J = \langle t \cdot f_i, , (1-t) \cdot g_j \rangle_{i,j} \cap R$$

where ${f_i}$ and ${g_j}$ are generators of $I$ and $J$ respectively.

Intuition. At $t = 1$ the constraints degenerate to $f_i = 0$ (the conditions of $I$); at $t = 0$ they degenerate to $g_j = 0$ (the conditions of $J$). After eliminating $t$, the polynomials satisfying both sets of conditions are obtained.

Correctness. $h \in I \cap J$ ⟺ $h = t \cdot h + (1-t) \cdot h$, where $t \cdot h \in t \cdot I$ and $(1-t) \cdot h \in (1-t) \cdot J$. Conversely, a polynomial in the elimination ideal vanishes at both $t = 0$ and $t = 1$, so it belongs to both $I$ and $J$. $\square$

Example. $\langle x \rangle \cap \langle y \rangle = \langle xy \rangle$.

In $\mathbb{Q}[x, y, t]$, compute $\text{GB}(tx, (1-t)y)$ and eliminate $t$:

  • $tx = 0 \implies t = 0$ or $x = 0$
  • $(1-t)y = 0 \implies t = 1$ or $y = 0$

Elimination yields $\langle xy \rangle$. $\square$

Ideal Saturation

Definition. The saturation of $I$ with respect to $J$ is

$$I : J^\infty = \bigcup_{k=1}^{\infty} (I : J^k)$$

Equivalently, $I : J^\infty$ is the "restriction" of $I$ away from $V(J)$ — it agrees with $I$ outside the zero set of $J$.

Iterative computation. By the ascending chain condition there exists $k_0$ with $I : J^{k_0} = I : J^{k_0+1} = \cdots = I : J^\infty$. The algorithm repeatedly computes $I : J$ until stabilization:

I_old ← I
loop:
    I_new ← I_old : J
    if I_new == I_old:  // compare Gröbner bases
        return I_new
    I_old ← I_new

Applications. Saturation is extremely important in algebraic geometry:

  • Radical computation: $\sqrt{I} = I : h^\infty$ (in the positive-dimensional case, $h$ is related to the Jacobian)
  • Primary decomposition: separating the components of different dimensions
  • Closure computation: projective closures, complete intersections

Example. $\langle x^2 y, xy^2 \rangle : \langle x \rangle^\infty$.

Computing the quotients round by round according to the mathematical definition:

First round: $\langle x^2 y, xy^2 \rangle : \langle x \rangle = \langle xy, y^2 \rangle$

Second round: $\langle xy, y^2 \rangle : \langle x \rangle = \langle y \rangle$

Third round: $\langle y \rangle : \langle x \rangle = \langle y \rangle$ (stable)

Hence $\langle x^2 y, xy^2 \rangle : \langle x \rangle^\infty = \langle y \rangle$. $\square$

(Note: oCAS's ideal_quotient computes $I : g^\infty$ in a single elimination step — in this example one call already yields $\langle y \rangle$, and the iteration in ideal_saturate merely confirms stability.)

Sums, Products, and Membership

These are the most basic ideal operations; they are comparatively simple to implement:

OperationDefinitionImplementation
$I + J$$\langle f_1, \ldots, f_m, g_1, \ldots, g_k \rangle$merge the generators and compute a GB
$I \cdot J$$\langle f_i g_j \rangle_{i,j}$all pairwise products of the generators, then compute a GB
$f \in I$ ?—compute $\text{NF}(f, G_I)$ and check whether it is zero

Primary Decomposition

Definition. An ideal $Q \subseteq R$ is called a primary ideal if $fg \in Q$ implies $f \in Q$ or $g^n \in Q$ (for some $n \geq 1$).

Primary decomposition theorem (Lasker–Noether). Every ideal $I \subseteq \mathbb{F}[x_1, \ldots, x_n]$ ($\mathbb{F}$ of characteristic zero) has a primary decomposition

$$I = Q_1 \cap Q_2 \cap \cdots \cap Q_r$$

where each $Q_i$ is a primary ideal. The corresponding associated prime ideals are $\mathfrak{p}_i = \sqrt{Q_i}$.

Zero-Dimensional Primary Decomposition

For zero-dimensional ideals, primary decomposition can be achieved by factoring the univariate polynomials in a Lex Gröbner basis.

Key observation. The Lex GB of a zero-dimensional ideal contains a polynomial $p_1(x_1)$ in $x_1$ alone, a polynomial $p_2(x_1, x_2)$ in $x_1, x_2$, and so on. The factorization of $p_1(x_1)$ corresponds to the different branches of the ideal in the $x_1$ direction.

Algorithm (matching the oCAS implementation primary_decomp_zero_dim):

  1. Compute the Lex Gröbner basis $G$ of $I$
  2. Extract the polynomial $p_1(x_1)$ in $x_1$ alone and compute its square-free part $\tilde{p}_1 = p_1 / \gcd(p_1, p_1')$
  3. Factor $\tilde{p}_1$ over $\mathbb{Q}$ into pairwise distinct irreducible factors $\tilde{p}_1 = q_1 \cdot q_2 \cdots q_s$
  4. For each factor $q_i$, separate the branch by saturation: $I_i = I : \left(\prod_{j \neq i} q_j\right)^\infty$ (the source saturates by each other factor sequentially)
  5. The associated prime ideal of branch $I_i$ is taken as $\mathfrak{p}_i = \mathrm{GB}(I + \langle q_i \rangle)$

Note. If $\tilde{p}_1$ has only one irreducible factor (or $G$ contains no polynomial in $x_1$ alone), the algorithm returns the single branch ${\text{primary} = I,; \text{prime} = \sqrt{I}}$. The current implementation handles only the factorization in the first variable $x_1$; it does not recurse into the remaining variables.

Example. $I = \langle x^2 - 1, y - x \rangle \subseteq \mathbb{Q}[x, y]$.

Lex GB: ${x^2 - 1, y - x}$. $p_1(x) = x^2 - 1$ is already square-free and factors as $q_1 = x - 1$, $q_2 = x + 1$.

  • Branch 1: $I_1 = I : \langle x + 1 \rangle^\infty = \langle x - 1, y - 1 \rangle$ (associated prime $\langle x - 1, y - 1 \rangle$)
  • Branch 2: $I_2 = I : \langle x - 1 \rangle^\infty = \langle x + 1, y + 1 \rangle$ (associated prime $\langle x + 1, y + 1 \rangle$)

Check: $\langle x^2 - 1, y - x \rangle = \langle x - 1, y - 1 \rangle \cap \langle x + 1, y + 1 \rangle$; both branches are prime.

For comparison, for $I = \langle x^2, xy \rangle$ the square-free part of $p_1(x) = x^2$ has only the single factor $x$, so the algorithm returns the single branch ${\text{primary} = \langle x^2, xy \rangle,; \text{prime} = \sqrt{I} = \langle x \rangle}$. The ideal nevertheless admits the finer decomposition $\langle x^2, xy \rangle = \langle x \rangle \cap \langle x^2, y \rangle$, where $\langle x \rangle$ is prime and $\langle x^2, y \rangle$ is primary with associated prime $\langle x, y \rangle$ (an embedded component).

Positive-Dimensional Primary Decomposition

Primary decomposition of positive-dimensional ideals is much more complicated; oCAS currently marks it as not yet implemented (TODO). A complete implementation requires:

  • the Gianni–Trager–Zacharias algorithm
  • or the characteristic method of Eisenbud–Huneke–Vasconcelos

Radicals

Definition. The radical of an ideal $I$ is

$$\sqrt{I} = {f \in R : f^n \in I \text{ for some } n \geq 1}$$

Equivalently, $\sqrt{I}$ is the largest ideal containing $I$ with $V(\sqrt{I}) = V(I)$ (the same zero set).

Zero-Dimensional Radicals

For zero-dimensional ideals, radical computation uses the square-free factorization of the univariate polynomials in the Lex GB.

Algorithm:

  1. Compute the Lex GB $G$
  2. For each polynomial $p_i(x_i)$ in $G$ involving only $x_i$, compute its square-free part $\tilde{p}_i = p_i / \gcd(p_i, p_i')$
  3. Replace $p_i$ by $\tilde{p}_i$ to obtain generators of $\sqrt{I}$

Correctness. The radical of a zero-dimensional ideal corresponds to reducing the exponent of each associated prime ideal to $1$. Square-free factorization achieves exactly this — the square-free part of $p_i^{e_i}$ is $p_i$.

Example. $\sqrt{\langle x^2, xy \rangle}$.

Lex GB: ${x^2, xy}$. The square-free part of $x^2$: $x^2 / \gcd(x^2, 2x) = x^2 / x = x$.

Radical: $\langle x, xy \rangle = \langle x \rangle$ (since $x$ already divides $xy$). $\square$

Positive-Dimensional Radicals: Jacobian Saturation

For positive-dimensional ideals, oCAS uses a Jacobian-based saturation method (a simplified Kemper algorithm):

$$\sqrt{I} = I : h^\infty$$

where $h$ is related to the Jacobian determinant.

Algorithm (characteristic zero, matching radical_via_jacobian):

  1. For each generator $f_i$ of $I$ and each variable $x_j$, compute the partial derivative $\partial f_i / \partial x_j$ (skipping constant/zero derivatives)
  2. Choose $h$ heuristically: the source does not compute the true gcd, but folds the non-trivial derivatives with reduce, keeping the one of smallest total degree. This is a heuristic approximation of the classical Jacobian-saturation formula ($\sqrt{I} = I : h^\infty$, where $h$ must be a suitable Jacobian-related element) — $h$ is just a single partial derivative (one entry of the Jacobian matrix); it is not guaranteed to divide the Jacobian determinant, nor is the saturation result guaranteed to equal $\sqrt{I}$
  3. Compute $I : h^\infty$ (iterative saturation)

Theoretical basis. At a regular point, the rank of the Jacobian matrix equals the dimension of the variety (Jacobian criterion); the singular points are exactly those where the rank is deficient (less than $n - \dim$) — where the exponent structure is non-trivial. The saturation $I : h^\infty$ serves to remove these powers.

Limitation. When all partial derivatives are constant/zero, or $h$ is a constant polynomial (total degree 0), the algorithm falls back to returning the original GB. Since $h$ is chosen heuristically rather than being a genuine Jacobian-related element, the saturation result is only an approximation of $\sqrt{I}$ (it may be too large, or too small and miss components) — for positive-dimensional ideals, a complete radical computation requires more refined algorithms (such as Gianni–Trager–Zacharias or Eisenbud–Huneke–Vasconcelos).

Testing Primality and Primaryness

Zero-dimensional primality test. A zero-dimensional ideal $I$ is prime if and only if every polynomial in its Lex GB involving only $x_i$ is irreducible.

oCAS implementation (is_prime_zero_dim):

  1. Compute the Lex GB
  2. Extract each univariate polynomial $p_i(x_i)$
  3. For polynomials of degree $\le 3$, check reducibility with the rational-root theorem (a polynomial of degree $d \le 3$ over $\mathbb{Q}$ is irreducible iff it has no rational root); polynomials of degree $> 3$ receive no full factorization check

Note. Positive-dimensional ideals currently conservatively return false — a prime positive-dimensional ideal is misreported as non-prime (false negative). In the zero-dimensional case, however, a reducible degree-$>3$ univariate polynomial without rational roots (such as $(x^2-2)(x^2-3)$) is misjudged as irreducible, so false positives (reporting a non-prime ideal as prime) are possible.

Primaryness test. An ideal $I$ is primary if and only if it has exactly one associated prime ideal. The implementation checks the number of components via primary decomposition.


Implementation in oCAS

The FGLM Implementation

The FGLM algorithm is implemented in ocas-poly/src/groebner/fglm.rs.

Entry Function

#![allow(unused)]
fn main() {
pub fn fglm<D: Domain, O2: MonomialOrder>(
    gb: &GroebnerBasis<D, impl MonomialOrder>,
) -> Option<GroebnerBasis<D, O2>>
}

Generic parameters:

  • D: the coefficient domain (implementing the Domain trait)
  • O2: the target monomial order
  • the source order of the input basis is determined by the type parameter impl MonomialOrder of gb

Returns None when the ideal is not zero-dimensional (the staircase computation finds infinitely many standard monomials).

Staircase Computation

#![allow(unused)]
fn main() {
fn compute_staircase(lms: &[Vec<usize>], n_vars: usize) -> Option<Vec<Vec<usize>>>
}

BFS algorithm:

  • start from the all-zero exponent vector $1 = x_1^0 \cdots x_n^0$
  • for each monomial $m$ in the queue, check whether it is divisible by some $\text{lm}(g_i)$
  • if not divisible → add it to the staircase and enqueue its $n$ neighbours (incrementing one variable exponent of $m$ at a time)
  • safety threshold 100,000: if the BFS visits more than this many monomials, the ideal is judged positive-dimensional

The divisibility check compares component by component:

#![allow(unused)]
fn main() {
fn monomial_divides_big(lm: &[usize], big: &[usize]) -> bool {
    lm.iter().zip(big.iter()).all(|(a, b)| a <= b)
}
}

Normal-Form Computation

#![allow(unused)]
fn main() {
fn normal_form_monomial<D: Domain>(
    m: &[usize],
    gb: &GroebnerBasis<D, impl MonomialOrder>,
    staircase: &[Vec<usize>],
    domain: &D,
) -> Vec<D::Element>
}

Build the monomial $m$ as the monomial polynomial $1 \cdot x^m$, divide it by the GB with remainder, and then map each term of the remainder to its position index in the staircase. Returns a coordinate vector of length $D$.

Linear-Relation Detection

#![allow(unused)]
fn main() {
fn find_relation<D: Domain>(
    seen: &[Vec<D::Element>],
    nf: &[D::Element],
    domain: &D,
) -> Option<Vec<D::Element>>
}

Build the augmented matrix $[\text{seen}^T \mid \text{nf}]$ ($D$ rows, $k+1$ columns) and perform forward elimination + back substitution:

  1. find a pivot (a non-zero element) column by column and swap rows
  2. normalize the pivot row and eliminate that column in the remaining rows
  3. check consistency: if some row has all zeros on the left but a non-zero right-hand side → no solution (linearly independent)
  4. extract the coefficient vector by back substitution

Returns Some(coeffs) when linearly dependent and None when linearly independent.

Marking Multiples

#![allow(unused)]
fn main() {
fn mark_multiples(
    visited: &mut HashMap<Vec<usize>, bool>,
    m: &[usize],
    n_vars: usize,
    max_deg: usize,
)
}

BFS marks all multiples of $m$ (with total degree at most max_deg) as visited. max_deg is set to $2D$ — large enough to cover all monomials that could produce new leading terms.

Main-Loop Details

The main loop maintains:

  • boundary: the set of candidate monomials (initially ${1}$, the all-zero exponent vector)
  • seen_nfs: the list of seen normal-form vectors
  • seen_mons: the corresponding list of monomials
  • new_basis: the output Gröbner basis

Each step takes the target-order-smallest monomial in the boundary, computes its normal form, and tests for linear dependence. If dependent → output the new polynomial and mark multiples; if independent → add it to seen and expand its neighbours.

Loop termination: the boundary is empty or the number of steps exceeds $D(D+1) + 4n$ (a safety bound).

Finally, minimize() (removing redundant polynomials) and auto_reduce() (mutual reduction) are applied to new_basis, producing the reduced Gröbner basis.

Implementing Ideal Operations

The ideal operations are implemented in ocas-poly/src/ideal.rs, uniformly using the Lex order to guarantee the elimination property.

Membership

#![allow(unused)]
fn main() {
pub fn ideal_contains<D: Domain + 'static>(
    generators: &[SparseMultivariatePolynomial<D, Lex>],
    f: &SparseMultivariatePolynomial<D, Lex>,
    algo: Algorithm,
) -> bool
}

Compute the GB, then compute the normal form of $f$ and check whether it is zero.

Ideal Quotient

#![allow(unused)]
fn main() {
pub fn ideal_quotient<D: Domain + 'static>(
    generators_i: &[SparseMultivariatePolynomial<D, Lex>],
    generators_j: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Internal flow:

  1. For each generator $g_j$ of $J$, call quotient_single_generator (the Rabinowitsch trick)
  2. quotient_single_generator implements:
    • lift the generators of $I$ into $R[w]$
    • add $1 - wg$
    • compute the Lex GB
    • eliminate $w$ (take the polynomials not involving $w$)
  3. Intersect the results of all $I : g_j^\infty$ (intersect_generators)

Ideal Intersection

#![allow(unused)]
fn main() {
pub fn ideal_intersection<D: Domain + 'static>(
    generators_a: &[SparseMultivariatePolynomial<D, Lex>],
    generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Internally calls intersect_generators:

  1. Introduce the auxiliary variable $t$ (index 0)
  2. Construct the generators ${t \cdot f_i} \cup {(1-t) \cdot g_j}$
  3. Compute the Lex GB in the extended ring $R[t]$
  4. Eliminate $t$ to obtain generators of $I \cap J$

Ideal Saturation

#![allow(unused)]
fn main() {
pub fn ideal_saturate<D: Domain + 'static>(
    generators_i: &[SparseMultivariatePolynomial<D, Lex>],
    generators_j: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex>
}

Iteratively calls ideal_quotient until the GB stabilizes (a two-way containment check: every basis element of each side reduces to zero against the other), with a cap of 20 rounds; the current result is returned when the cap is exceeded.

Primary Decomposition

#![allow(unused)]
fn main() {
pub fn primary_decomposition(
    generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> Vec<PrimaryComponent>
}

Currently only the zero-dimensional case is implemented (primary_decomp_zero_dim), and only the first variable is handled:

  1. Compute the Lex GB
  2. Extract the univariate polynomial $p_1(x_1)$ in $x_1$ alone and compute its square-free part
  3. Factor the square-free part over $\mathbb{Q}$
  4. Separate the branches by saturation (sequentially saturating by each other factor via ideal_saturate)
  5. The associated prime of a branch is taken as $\mathrm{GB}(I + \langle q_i \rangle)$; a single factor yields the single branch ($\text{prime} = \sqrt{I}$)

The PrimaryComponent struct contains:

#![allow(unused)]
fn main() {
pub struct PrimaryComponent {
    pub primary: GroebnerBasis<RationalDomain, Lex>,  // the primary ideal
    pub prime: GroebnerBasis<RationalDomain, Lex>,     // the associated prime ideal
}
}

Radicals

#![allow(unused)]
fn main() {
pub fn ideal_radical(
    generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> GroebnerBasis<RationalDomain, Lex>
}

Two cases:

  • Zero-dimensional (radical_zero_dim): apply square-free factorization to each univariate polynomial in the Lex GB, replace them, and regenerate the GB
  • Positive-dimensional (radical_via_jacobian): compute the non-trivial partial derivatives of all generators with respect to all variables, take $h$ as the one of smallest total degree (a heuristic, not the true gcd), and compute $I : h^\infty$

Other Tests

#![allow(unused)]
fn main() {
pub fn is_zero_dimensional(gb: &GroebnerBasis<RationalDomain, Lex>) -> bool
pub fn is_prime_ideal(generators: &[...]) -> bool
pub fn is_primary_ideal(generators: &[...]) -> bool
}
  • is_zero_dimensional: checks whether every variable has a pure-power leading term
  • is_prime_ideal: zero-dimensional: check irreducibility; positive-dimensional: conservatively return false
  • is_primary_ideal: checks whether there is exactly one associated prime ideal

Solving Zero-Dimensional Systems

#![allow(unused)]
fn main() {
pub fn solve_polynomial_system(
    equations: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
    algo: Algorithm,
) -> PolynomialSystemSolution
}

Algorithm flow:

  1. Compute the Lex GB (if the input is not already a GB)
  2. Check zero-dimensionality
  3. Solve by triangular decomposition (solve_triangular) — back-substitution starting from the smallest variable in the Lex order ($x_n$):
    • extract the polynomial in $x_n$ alone → isolate real roots and find them numerically
    • for each root, substitute into the polynomial involving $x_{n-1}$ → find the roots of $x_{n-1}$
    • recurse until all variables are determined (the results are returned in the order $x_1, \ldots, x_n$)

Return type:

#![allow(unused)]
fn main() {
pub enum PolynomialSystemSolution {
    ZeroDimensional(ZeroDimSolutions),  // finitely many solutions
    PositiveDimensional(GroebnerBasis),  // positive-dimensional (infinitely many solutions)
    Empty,                               // no solutions
}
}

Advanced Topics

Choosing Between FGLM and F4

ScenarioRecommended methodReason
Zero-dimensional + Lex GB neededGrevlex F4 → FGLM$O(n \cdot D^3)$ is far faster than Lex F4
Zero-dimensional + Grevlex GB neededF4 directlyno order change needed
Positive-dimensional + Lex GB neededLex F4 or reorderFGLM does not apply
Elimination + zero-dimensionalFGLM → eliminationthe elimination theorem guarantees correctness
Elimination + positive-dimensionalLex F4FGLM does not apply

FGLM over Arbitrary Fields

oCAS's FGLM implementation works generically over an arbitrary field $D$; normal-form computation goes entirely through the operations of the Domain trait (finite-field elements are stored as canonical representatives in $[0, p)$). Note that the FpPoly i64 fast path of F4/F5 for $\mathbb{F}_p$ is not used by FGLM.

Limitations

  1. Zero-dimensional only: the staircase of a positive-dimensional ideal is infinite, so FGLM does not apply
  2. The $D^3$ bottleneck: when the vector-space dimension $D$ is very large (e.g. $D > 10^4$), Gaussian elimination becomes the bottleneck
  3. Memory: the $D \times D$ multiplication matrices and the augmented matrix must be stored

Relation to the Literature

The oCAS implementation is based directly on the original papers:

  • FGLM: Faugère, Gianni, Lazard, Mora (1993), Efficient Computation of Zero-dimensional Gröbner Bases by Change of Ordering, JSC
  • Ideal operations: Cox, Little, O'Shea, Ideals, Varieties, and Algorithms, Chapters 4, 8
  • Primary decomposition: Gianni, Trager, Zacharias (1988), Gröbner Bases and Primary Decomposition of Polynomial Ideals
  • Radical computation: Kemper (2002), A Course in Commutative Algebra

References

  1. J.-C. Faugère, P. Gianni, D. Lazard, T. Mora. "Efficient Computation of Zero-dimensional Gröbner Bases by Change of Ordering." Journal of Symbolic Computation, 16(4):329–344, 1993.
  2. D. Cox, J. Little, D. O'Shea. Ideals, Varieties, and Algorithms. 4th ed., Springer, 2015. Chapters 3 (elimination), 4 (ideal quotients and saturation), 8 (primary decomposition).
  3. W. W. Adams, P. Loustaunau. An Introduction to Gröbner Bases. AMS, 1994.
  4. P. Gianni, B. Trager, G. Zacharias. "Gröbner Bases and Primary Decomposition of Polynomial Ideals." Journal of Symbolic Computation, 6(2–3):149–167, 1988.
  5. G. Kemper. A Course in Commutative Algebra. Springer, 2011.
  6. T. Becker, V. Weispfenning. Gröbner Bases: A Computational Approach to Commutative Algebra. Springer, 1993.

See also:

Algebraic Number Fields & Galois Theory

Algebraic number fields are the central structures of computer algebra connecting polynomial algebra with number theory. In oCAS, an algebraic number field $\mathbb{Q}(\alpha)$ is not only a coefficient field — allowing polynomial arithmetic and factorization over the extension field — but also a unified abstraction for Galois fields $\mathrm{GF}(p^d)$. This chapter starts from the basic concepts of minimal polynomials and field extensions, systematically presents the arithmetic on algebraic number fields, norm algorithms, and modular methods, and shows how these theories map onto the oCAS implementation.

Prerequisites

Before reading this chapter, we recommend studying:

Minimal Polynomials

Let $K$ be an extension field of $\mathbb{Q}$ and $\alpha \in K$. If there exists a non-zero polynomial $f \in \mathbb{Q}[x]$ with $f(\alpha) = 0$, then $\alpha$ is called an algebraic element. The monic polynomial $m(x)$ of lowest degree satisfying this condition is called the minimal polynomial of $\alpha$.

Properties:

  1. The minimal polynomial is irreducible in $\mathbb{Q}[x]$.
  2. Every polynomial $f$ with $f(\alpha) = 0$ satisfies $m \mid f$.
  3. The minimal polynomial is unique up to isomorphism.

Example: the minimal polynomial of $\sqrt{2}$ is $x^2 - 2$, since $(\sqrt{2})^2 - 2 = 0$ and $x^2 - 2$ is irreducible over $\mathbb{Q}$ (Eisenstein's criterion, $p = 2$).

Field Extensions and Extension Degree

Given fields $F \subseteq K$, one calls $K$ a field extension of $F$, written $K/F$. The dimension of $K$ as an $F$-vector space is called the extension degree:

$$[K : F] = \dim_F K$$

When $K = F(\alpha)$ (generated by a single algebraic element), it is called a simple extension. The key theorem:

$$[F(\alpha) : F] = \deg(m_\alpha)$$

where $m_\alpha$ is the minimal polynomial of $\alpha$ over $F$. Hence ${1, \alpha, \alpha^2, \ldots, \alpha^{d-1}}$ ($d = [K:F]$) is a basis of $K$ as an $F$-vector space.

Field Towers and the Tower Law

For a tower of fields $F \subseteq K \subseteq L$, the extension degrees satisfy the tower law:

$$[L : F] = [L : K] \cdot [K : F]$$

Example: let $\alpha = \sqrt[3]{2}$ and $\omega = e^{2\pi i/3}$. Then $\mathbb{Q}(\alpha, \omega) / \mathbb{Q}(\alpha)$ has degree 2 ($\omega$ satisfies $x^2 + x + 1$), $\mathbb{Q}(\alpha) / \mathbb{Q}$ has degree 3, and therefore $[\mathbb{Q}(\alpha, \omega) : \mathbb{Q}] = 6$.


Basic Concepts

Representation of Elements of $\mathbb{Q}(\alpha)$

Let $m(x) = x^d + a_{d-1}x^{d-1} + \cdots + a_0$ be the minimal polynomial of $\alpha$, with $d = [K:\mathbb{Q}]$. From $m(\alpha) = 0$ we obtain

$$\alpha^d = -(a_{d-1}\alpha^{d-1} + \cdots + a_0)$$

so every element of $\mathbb{Q}(\alpha)$ can be represented uniquely as

$$c_0 + c_1 \alpha + c_2 \alpha^2 + \cdots + c_{d-1} \alpha^{d-1}, \quad c_i \in \mathbb{Q}$$

i.e. as a polynomial in $\alpha$ of degree $< d$. This representation is unique, because the difference of two distinct polynomials of degree $< d$ would be a non-zero multiple of $m(x)$ (a polynomial of degree $< d$ cannot be a multiple of $m$ unless it is zero).

Addition and scalar multiplication operate term by term on the coefficients. Multiplication first multiplies the polynomials (obtaining a polynomial of degree $\le 2(d-1)$) and then reduces modulo $m(\alpha)$ — repeatedly replacing $\alpha^d$ by $-\sum_{i=0}^{d-1} a_i \alpha^i$ until the degree is $< d$.

Example: $\mathbb{Q}(\sqrt{2})$ with $m(x) = x^2 - 2$. Elements are $a + b\sqrt{2}$ ($a, b \in \mathbb{Q}$). Multiplication:

$$(1 + \sqrt{2})(3 - \sqrt{2}) = 3 + 3\sqrt{2} - \sqrt{2} - 2 = 1 + 2\sqrt{2}$$

Computing Inverses

An element $a(\alpha) \in \mathbb{Q}(\alpha)$ is invertible if and only if $a(\alpha) \ne 0$. The key tool for computing inverses is the extended Euclidean algorithm: applying it to $a(x)$ and $m(x)$ in $\mathbb{Q}[x]$ gives

$$s(x) \cdot a(x) + t(x) \cdot m(x) = g(x)$$

where $g = \gcd(a, m)$. Since $m$ is irreducible and $a \not\equiv 0 \pmod{m}$, we must have $\deg(g) = 0$, i.e. $g$ is a non-zero constant. After normalizing $g = 1$, we get

$$a(\alpha)^{-1} = s(\alpha) \bmod m(\alpha)$$

Example: compute $(1 + \sqrt{2})^{-1}$ in $\mathbb{Q}(\sqrt{2})$.

Use the extended Euclidean algorithm to find $s(x)(1+x) + t(x)(x^2-2) = 1$:

  • $x^2 - 2 = (x - 1)(1 + x) + (-1)$
  • hence $(-1) = (x^2 - 2) - (x-1)(1+x)$
  • normalizing: $1 = (x-1)(1+x) - (x^2-2)$

So $s(x) = x - 1$; verification: $(1+\sqrt{2})(\sqrt{2}-1) = \sqrt{2}^2 - 1 = 1$. ✓


Core Theory

Construction of $\mathrm{GF}(p^d)$

The mod-$p$ "projection" of an algebraic number field needs the finite field $\mathrm{GF}(p^d)$. The construction is completely parallel to that of $\mathbb{Q}(\alpha)$:

  1. Take an irreducible polynomial $\bar{m}(x)$ of degree $d$ in $\mathbb{F}_p[x]$ (usually the image of the minimal polynomial $m(x)$ modulo $p$).
  2. $\mathrm{GF}(p^d) = \mathbb{F}_p[x]/(\bar{m}(x))$.
  3. Elements are $c_0 + c_1\alpha + \cdots + c_{d-1}\alpha^{d-1}$ with $c_i \in \mathbb{F}_p$.
  4. The operations are the same as for $\mathbb{Q}(\alpha)$, except that coefficients are reduced modulo $\mathbb{F}_p$.

Key condition: $m(x) \bmod p$ must still be an irreducible polynomial of degree $d$. If $p$ divides a denominator of $m$ or makes $m \bmod p$ degenerate (lower degree or reducible), this prime cannot be used for modular methods.

Example: $m(x) = x^2 + 1$ (the minimal polynomial of $\mathbb{Q}(i)$). Over $\mathbb{F}_3$, $x^2 + 1$ is irreducible (since $0^2+1=1$, $1^2+1=2$, $2^2+1=2$ are all non-zero), so $\mathrm{GF}(3^2) = \mathbb{F}_3[x]/(x^2+1)$, whose multiplicative group has order 8.

Trager's Norm Algorithm

Let $K = \mathbb{Q}(\alpha)$ and let $f \in K[x]$ be square-free. Trager's algorithm solves the factorization problem by "reducing" it from $K[x]$ to $\mathbb{Q}[x]$. The core tool is the norm map.

Definition of the Norm

For $f \in K[x]$, its norm $N(f) \in \mathbb{Q}[x]$ is defined as:

$$N(f)(x) = \operatorname{Res}_\alpha\bigl(m(\alpha),, f(x, \alpha)\bigr)$$

i.e. viewing $f(x, \alpha)$ as a polynomial in $\alpha$ (with coefficients in $\mathbb{Q}[x]$), take its resultant with $m(\alpha)$ (eliminating $\alpha$).

Key property: if $f$ is irreducible in $K[x]$, then $N(f)$ is a power of an irreducible polynomial in $\mathbb{Q}[x]$. More precisely, if $f = g_1 g_2 \cdots g_r$ is the irreducible factorization in $K[x]$, then

$$N(f) = N(g_1) \cdot N(g_2) \cdots N(g_r)$$

and each $N(g_i)$ is irreducible in $\mathbb{Q}[x]$.

Computing the Norm by Evaluation–Interpolation

Computing the resultant $\operatorname{Res}_\alpha(m, f(x,\alpha))$ directly is expensive for high-degree polynomials. oCAS uses the evaluation–interpolation method:

  1. The norm $N(f)$, as a polynomial in $x$, has degree $\deg_x(f) \cdot [K:\mathbb{Q}]$.
  2. Take $n = \deg_x(f) \cdot d + 1$ rational points $x_0, x_1, \ldots, x_{n-1}$ (using the symmetric sequence $0, -1, 1, -2, 2, \ldots$ to keep the arithmetic small).
  3. At each $x_j$, compute $f(x_j, \alpha) \in K$ (Horner evaluation), then compute the scalar resultant $\operatorname{Res}_\alpha(m, f(x_j, \alpha))$.
  4. Recover $N(f)$ by Newton divided-difference interpolation.

This is exact (rational arithmetic without rounding) and avoids the direct computation of a high-degree resultant.

The Trager Shift

Even if $f$ is square-free, its norm $N(f)$ may have square factors (e.g. the norm of $f = x - \alpha$ over $\mathbb{Q}(\sqrt{2})$ is $x^2 - 2$, already square-free; but the norm of $f = x^2 - 2$ is $(x^2-2)^2$, which has squares).

Trager shift: apply the substitution $x \mapsto x - s\alpha$ to $f(x)$ ($s \ge 0$), obtaining $g(x) = f(x - s\alpha)$. Since only finitely many $s$ are "bad" (make $N(g)$ non-square-free), an $s$ with square-free $N(g)$ always exists.

Algorithm:

for s = 0, 1, 2, ...:
    g(x) ← f(x − s·α)          // Horner composition
    R(x) ← N(g)(x)              // evaluation–interpolation norm
    if R(x) is square-free (verified modulo primes):
        return (s, g, R)

oCAS tries at most MAX_TRAGER_SHIFTS = 100 shift values. Square-freeness is verified by a modular method: for several primes $p$, if $R \bmod p$ is square-free in $\mathbb{F}_p[x]$, then $R$ is square-free in $\mathbb{Q}[x]$ (a sufficient but not necessary condition — which is exactly the "acceptance" criterion we need).

The Factorization Flow

Given a square-free $f \in K[x]$:

  1. Find a shift: $(s, g, R) \leftarrow \text{norm_with_shift}(f)$.
  2. Factor the norm: $R = r_1 \cdot r_2 \cdots r_k$ (irreducible factorization in $\mathbb{Q}[x]$).
  3. Pull back to $K$: for each $r_i$, compute $\gcd_K(g, r_i)$ (embedding $r_i$ into $K[x]$) to obtain the factors of $f$ in $K[x]$.
  4. Undo the shift: $x \mapsto x + s\alpha$ to restore the original variable.

Correctness guarantee: the factors $r_i$ of the norm correspond to factors of $g$, and the GCD in $K[x]$ extracts these factors exactly.

Modular GCD over Number Fields (Encarnación's Method)

The $\gcd_K$ in step 3 (the GCD of two polynomials in $K[x]$) is the computational bottleneck. Doing the Euclidean GCD directly in $K[x]$ causes severe coefficient growth (the coefficients are elements of $\mathbb{Q}(\alpha)$, i.e. $\alpha$-polynomials over the rationals).

The modular method (Encarnación) projects the problem onto $\mathrm{GF}(p^d)$:

Input: a, b ∈ K[x] (K = ℚ(α))
Output: monic gcd(a, b) ∈ K[x]

1. CRT state: residues = None, modulus = 1
2. for p in primes_from(2):
3.     if m(x) mod p is reducible or loses degree: skip  // m degenerates over F_p; this prime is unusable
4.     GF ← GF(p^d) = F_p[x]/(m mod p)
5.     if p | lc(a) or p | lc(b): skip
6.     g_p ← monic(gcd(a mod GF, b mod GF))   // GCD over GF(p^d)[x]
7.     if deg(g_p) > best_deg: skip            // unlucky prime
8.     if deg(g_p) < best_deg:
9.         best_deg ← deg(g_p)
10.        residues ← coefficient table of g_p  // [x-degree][α-degree] F_p values
11.        modulus ← p
12.    else:
13.        residues ← CRT(residues, g_p)  // merge coefficient by coefficient
14.        modulus ← modulus × p
15.
16.    // rational reconstruction + trial division verification
17.    candidate ← rational_reconstruct(residues, modulus)
18.    if candidate | a and candidate | b:
19.        return candidate
20.
21. return dense_euclidean_gcd(a, b)  // safe fallback

Prime Selection

Not every prime is suitable for the modular method. The requirements are:

  1. $m(x) \bmod p$ stays irreducible with the same degree: otherwise $\mathbb{F}_p[\alpha]/(m) \ne \mathrm{GF}(p^d)$ and the GCD semantics are wrong.
  2. $p$ does not divide the leading coefficients: otherwise the mapped polynomials drop in degree.
  3. $p$ does not divide any coefficient denominator: the map from the rationals to $\mathbb{F}_p$ would fail.

Detecting Unlucky Primes

If the modular GCD over a prime $p$ has degree higher than the current best, the prime is "unlucky" — $p$ divides a coefficient of some proper factor. The result is simply discarded.

If the modular GCD degree is lower than the current best, all previous primes were unlucky; discard the accumulated state and restart with the new result.

Rational Reconstruction

The residues after CRT combination are large integers (modulo a large composite). For each coefficient (each component of the $\alpha$-polynomial), rational reconstruction recovers a rational number from the integer pair:

Given an integer $a$ and a modulus $m$, find a rational number $n/d$ such that $a \equiv n/d \pmod{m}$ with $|n|, |d| \le \sqrt{m/2}$.

The Wang/extended-Euclid method tracks the sequence $(r_i, t_i)$ until $|r_1|, |t_1| \le \sqrt{m/2}$, verifying that $a \cdot d \equiv n \pmod{m}$.

Trial Division Verification

The candidate GCD from rational reconstruction must be verified by trial division: check that it divides both original inputs $a$ and $b$. Only verified results are returned.

oCAS tries at most MAX_ANF_GCD_PRIMES = 1000 primes. In practice more than a few dozen are rarely needed. In the worst case it falls back to the dense Euclidean GCD over $K[x] = \mathbb{Q}(\alpha)[x]$ (slow but guaranteed correct).

Cyclotomic Fields $\mathbb{Q}(\zeta_n)$

The number field $\mathbb{Q}(\zeta_n)$ generated by a primitive $n$-th root of unity $\zeta_n = e^{2\pi i/n}$ is called a cyclotomic field.

The Minimal Polynomial

The minimal polynomial of $\zeta_n$ is the cyclotomic polynomial:

$$\Phi_n(x) = \prod_{\substack{1 \le k \le n \ \gcd(k,n)=1}} \left(x - e^{2\pi i k/n}\right)$$

Its degree is $\varphi(n)$ (Euler's totient function). $\Phi_n$ is irreducible in $\mathbb{Q}[x]$.

Examples:

  • $\Phi_1(x) = x - 1$
  • $\Phi_2(x) = x + 1$
  • $\Phi_3(x) = x^2 + x + 1$
  • $\Phi_4(x) = x^2 + 1$
  • $\Phi_6(x) = x^2 - x + 1$
  • $\Phi_p(x) = x^{p-1} + x^{p-2} + \cdots + 1$ ($p$ prime)

The cyclotomic polynomials satisfy $x^n - 1 = \prod_{d \mid n} \Phi_d(x)$ and can be computed factor by factor using Möbius inversion.

Properties of the Field

  • $[\mathbb{Q}(\zeta_n) : \mathbb{Q}] = \varphi(n)$.
  • $\mathbb{Q}(\zeta_n)$ is an Abelian extension of $\mathbb{Q}$ (its Galois group is $(\mathbb{Z}/n\mathbb{Z})^*$).
  • Its ring of integers is $\mathbb{Z}[\zeta_n]$; the discriminant depends on the prime factors of $n$.

Example: the extension degree of $\mathbb{Q}(\zeta_5)$ is $\varphi(5) = 4$, with minimal polynomial $\Phi_5(x) = x^4 + x^3 + x^2 + x + 1$. Elements are represented as $a_0 + a_1\zeta + a_2\zeta^2 + a_3\zeta^3$ ($a_i \in \mathbb{Q}$).

In oCAS, constructing a cyclotomic field only requires specifying the cyclotomic polynomial:

#![allow(unused)]
fn main() {
use ocas_domain::{AlgebraicNumberField, Domain, Rational, RationalDomain};

// Q(ζ_5): minimal polynomial x^4 + x^3 + x^2 + x + 1
// ascending powers: [1, 1, 1, 1, 1]
let field = AlgebraicNumberField::new(
    RationalDomain,
    vec![
        Rational::new(1, 1),
        Rational::new(1, 1),
        Rational::new(1, 1),
        Rational::new(1, 1),
        Rational::new(1, 1),
    ],
);
let zeta = field.alpha(); // ζ_5
// ζ_5^5 = 1 (guaranteed by the minimal polynomial)
let z5 = field.pow(&zeta, 5);
assert_eq!(z5, field.one());
}

Implementation in oCAS

Core Data Structures

The algebraic number field implementation lives in ocas-domain/src/algebraic.rs.

AlgebraicElement<E>

A field element — the residue class of the minimal polynomial, stored as a unique polynomial representative of degree $< d$:

#![allow(unused)]
fn main() {
pub struct AlgebraicElement<E> {
    coeffs: Vec<E>,  // ascending powers, trailing zeros trimmed
}
}
  • The empty vector represents zero.
  • PartialEq compares the coefficient vectors directly (semantic equality = structural equality, because the representative is unique).
  • Display outputs from highest to lowest degree: (c_d)·α^d + … + (c_1)·α + c_0 (the constant term has no parentheses; e.g. 1 + 2α displays as (2)·α + 1).
#![allow(unused)]
fn main() {
// accessing the coefficients
let e = field.element(vec![r(1, 1), r(2, 1)]); // 1 + 2α
assert_eq!(e.coeffs(), &[r(1, 1), r(2, 1)]);
}

AlgebraicExtension<D>

The field extension $D[\alpha]/(m(\alpha))$:

#![allow(unused)]
fn main() {
pub struct AlgebraicExtension<D: Domain> {
    base: D,              // base field (RationalDomain or FiniteField)
    min_poly: Vec<D::Element>,  // minimal polynomial (ascending powers, monic)
}
}

Construction:

#![allow(unused)]
fn main() {
// Q(√2): m(x) = x² − 2, ascending powers [−2, 0, 1]
let field = AlgebraicNumberField::new(
    RationalDomain,
    vec![Rational::new(-2, 1), Rational::new(0, 1), Rational::new(1, 1)],
);
}

Invariants (checked by debug_assert):

  • The minimal polynomial has degree $\ge 1$ (at least 2 coefficients).
  • The minimal polynomial is monic (leading coefficient = 1).
  • Irreducibility is not verified: a ring modulo a reducible modulus has zero divisors; inv returns None for non-units.

Key methods:

MethodDescription
extension_degree()$[K:\mathbb{Q}] = \deg(m)$
min_poly()minimal polynomial coefficients (ascending powers)
base_domain()the base field $D$
alpha()the generator $\alpha$
from_base(c)embedding of a base-field element
element(coeffs)construct from coefficients (automatically reduced modulo $m$)

AlgebraicNumberField

A type alias:

#![allow(unused)]
fn main() {
pub type AlgebraicNumberField = AlgebraicExtension<RationalDomain>;
}

Arithmetic Operations

AlgebraicExtension<D> implements the Domain and EuclideanDomain traits.

Addition/subtraction: add/subtract the coefficient polynomials term by term.

Multiplication: multiply the polynomials, then reduce modulo the minimal polynomial (reduce). The reduction process:

while v.len() > d:
    c ← v.pop()                    // top coefficient
    offset ← v.len() − d
    for i in 0..d:
        v[offset + i] −= c · m[i] // subtract the expansion of c·α^d

This exploits the monicity of $m$ (leading coefficient = 1); no division is needed.

Inversion: the extended Euclidean algorithm. Compute $s(x) \cdot a(x) + t(x) \cdot m(x) = 1$ and return $s(\alpha) \bmod m(\alpha)$. If $\gcd(a, m) \ne 1$ ($a$ is a zero divisor), return None.

Division: div(a, b) = a * inv(b). Division fails when inv(b) is None.

EuclideanDomain: over a field, division with remainder degenerates to exact division (the remainder is always zero). GCD degenerates to: return zero if both are zero, otherwise return one.

Factorization over Number Fields (Trager's Algorithm)

The factorization implementation lives in ocas-poly/src/factor/algebraic.rs.

Overall Flow

Input: f ∈ K[x] (K = ℚ(α))
Output: [(g₁, e₁), (g₂, e₂), …] such that f = lc(f) · ∏ gᵢ^eᵢ

1. if all coefficients of f are ℚ-constants:
       // fast path: factor over ℚ first, then split each factor over K
       f_Q ← drop the α-components (then clear denominators and take the primitive part)
       for (h, e) in factor_primitive(f_Q):
           for g in factor_square_free_anf(K, embed(h)):
               output (g, e)
       return
2. for (g, e) in square_free_anf(K, f):      // Yun square-free factorization
3.     for h in factor_square_free_anf(K, g): // Trager factorization
4.         output (h, e)

Square-Free Factorization (square_free_anf)

Implements Yun's algorithm over $K[x]$, but uses gcd_anf (modular GCD) instead of the generic pseudo-remainder GCD. This is because the coefficients of elements of $\mathbb{Q}(\alpha)$ are rational numbers, and the coefficient growth of the pseudo-remainder GCD is even worse over arbitrary-precision rationals.

Trager Factorization (factor_square_free_anf)

#![allow(unused)]
fn main() {
fn factor_square_free_anf(field, f):
    if deg(f) <= 1:
        return [monic(f)]
    (s, g, R) ← norm_with_shift(field, f)  // find a shift making the norm square-free
    rational_factors ← factor_square_free_rationals(R)  // factor the norm over ℚ
    if len(rational_factors) <= 1:
        return [monic(f)]  // norm irreducible → f irreducible
    remaining ← g
    out ← []
    back_shift ← s·α  // inverse of the shift
    for r_i in rational_factors:
        r_i_K ← embed(r_i)  // embed into K[x]
        h ← gcd_anf(field, r_i_K, remaining)  // modular GCD
        remaining ← remaining / h
        out.append(monic(compose_linear(h, back_shift)))  // undo the shift
    return out
}

Modular GCD (gcd_anf)

A complete implementation of Encarnación's method. The CRT state tracks the $\alpha$-components of every coefficient:

#![allow(unused)]
fn main() {
// CRT state: residues[x-degree][α-degree] are integer residues
let mut residues: Option<Vec<Vec<Integer>>> = None;
let mut modulus = Integer::from(1);
}

Each iteration:

  1. Take the next prime $p$ and check whether $m \bmod p$ is irreducible (min_poly_irreducible_mod: map the coefficients to $\mathbb{F}_p$ and verify the degree and irreducibility).
  2. Construct $\mathrm{GF}(p^d) = \mathbb{F}_p[\alpha]/(m \bmod p)$.
  3. Map $a, b$ to $\mathrm{GF}(p^d)[x]$ and compute the monic GCD.
  4. Merge into the CRT state.
  5. Try rational reconstruction + trial division verification.

Unlucky-prime handling: compare the modular GCD degree with the historical best. Higher → discard; lower → reset.

Fallback: after more than MAX_ANF_GCD_PRIMES = 1000 primes, use the dense Euclidean GCD in $K[x]$.

Mapping from $\mathbb{Q}$ to $\mathrm{GF}(p^d)$

Mapping a rational number to $\mathbb{F}_p$:

#![allow(unused)]
fn main() {
fn map_rational_fp(c: &Rational, fp: &FiniteField) -> Option<FiniteFieldElement> {
    let num = fp.element(c.numer().to_bigint());  // numerator mod p
    let den = fp.element(c.denom().to_bigint());  // denominator mod p
    fp.div(&num, &den)  // num · den^{-1} mod p; returns None if the denominator is zero
}
}

Mapping an element of $\mathbb{Q}(\alpha)$ to $\mathrm{GF}(p^d)$:

#![allow(unused)]
fn main() {
fn map_element_gf(e: &AlgebraicElement<Rational>, gf: &GaloisField) -> Option<...> {
    e.coeffs().iter().map(|c| map_rational_fp(c, gf.base_domain())).collect()
}
}

Mapping an $\alpha$-polynomial to $\mathrm{GF}(p^d)[x]$: map coefficient by coefficient.

Shifts and Composition

compose_linear(f, shift) computes $f(x + \text{shift})$ by Horner's method:

#![allow(unused)]
fn main() {
fn compose_linear<D: Domain>(f: &UP<D>, shift: &D::Element) -> UP<D> {
    let domain = f.domain().clone();
    // linear = x + shift
    let linear = UP::from_coeffs(domain.clone(), vec![shift.clone(), domain.one()]);
    let mut acc = UP::from_coeffs(domain.clone(), Vec::new()); // zero polynomial
    for c in f.coeffs().iter().rev() {
        // acc ← acc · (x + shift) + c (polynomial Horner composition)
        acc = acc
            .mul(&linear)
            .add(&UP::from_coeffs(domain.clone(), vec![c.clone()]));
    }
    acc
}
}

In Trager's algorithm, the shift is $x \mapsto x - s\alpha$ and the inverse shift is $x \mapsto x + s\alpha$.

Norm Computation

norm_eval_interp implements the evaluation–interpolation norm:

#![allow(unused)]
fn main() {
fn norm_eval_interp(field: &AlgebraicNumberField, g: &UP<AlgebraicNumberField>) -> UP<RationalDomain> {
    let deg_x = g.degree().unwrap_or(0);
    let d = field.extension_degree();
    let n_points = d * deg_x + 1;  // number of points needed for interpolation
    // symmetric points 0, −1, 1, −2, 2, ...
    for j in 0..n_points {
        let x = Rational::new(((j+1)/2) * sign, 1);
        // Horner evaluation g(x) ∈ K
        let val = horner_eval(g, field.from_base(x));
        // scalar resultant Res_α(m, val(α))
        let norm_val = m_poly.resultant(&val_as_poly);
        xs.push(x); ys.push(norm_val);
    }
    interpolate_rational(&xs, &ys)  // Newton divided-difference interpolation
}
}

The Unified Abstraction of Galois Fields

The generic design of AlgebraicExtension lets $\mathbb{Q}(\alpha)$ and $\mathrm{GF}(p^d)$ share the same code:

TypeBase field $D$Minimal polynomialPurpose
AlgebraicExtension<RationalDomain>$\mathbb{Q}$irreducible in $\mathbb{Q}[x]$exact arithmetic
AlgebraicExtension<FiniteField>$\mathbb{F}_p$irreducible in $\mathbb{F}_p[x]$the "working field" of modular methods

In gcd_anf, the GCD computation for the same prime $p$ is carried out in $\mathrm{GF}(p^d)[x]$, and the results are then combined back into $\mathbb{Q}(\alpha)[x]$ by CRT.


Advanced Topics

Reducible Moduli and Zero Divisors

When $m(x) \bmod p$ is reducible over $\mathbb{F}_p$, the ring $\mathbb{F}_p[x]/(m)$ is not a field — it has zero divisors. oCAS does not verify irreducibility of the minimal polynomial and allows constructing the ring modulo a reducible modulus. In that case:

  • inv returns None for non-zero zero divisors (the extended Euclidean algorithm gives $\gcd \ne 1$).
  • gcd_anf skips such primes (the min_poly_irreducible_mod check).

Example: $m(x) = x^2 - 1 = (x-1)(x+1)$ is reducible over $\mathbb{Q}$. The ring $\mathbb{Q}[x]/(x^2-1)$ has the zero divisor $\alpha - 1$ (since $(\alpha-1)(\alpha+1) = \alpha^2-1 = 0$).

#![allow(unused)]
fn main() {
let field = AlgebraicNumberField::new(
    RationalDomain,
    vec![Rational::new(-1, 1), Rational::new(0, 1), Rational::new(1, 1)],
);
let alpha = field.alpha();
let a = field.sub(&alpha, &field.one());
assert!(field.inv(&a).is_none()); // α − 1 is a zero divisor
}

Efficiency of Prime Selection for Modular GCD

In practice, most small primes satisfy the condition that $m \bmod p$ is irreducible. For a minimal polynomial of degree $d$, the probability that a random degree-$d$ polynomial over $\mathbb{F}_p$ is irreducible is about $1/d$ (by the formula counting irreducible polynomials). On average, roughly $d$ primes must be tried to find a usable one.

oCAS iterates over primes starting from $p = 2$ (using the primes_from iterator). For larger $d$ (e.g. $d \ge 6$), more primes may need to be skipped, but the budget of 1000 primes is almost always sufficient.

The Trade-off Between Coefficient Growth and Rational Reconstruction

The core advantage of the modular method is that it avoids coefficient growth: arithmetic over $\mathrm{GF}(p^d)$ is modular and the coefficients stay bounded. However, the residues after CRT combination grow as $O(\prod p_i)$, and rational reconstruction requires the bound $\sqrt{m/2}$.

In practice, if the coefficients of the true GCD are small (the typical case), 2–5 primes usually suffice. If the coefficients are large (e.g. a complicated GCD over a high-degree extension), more primes may be needed to accumulate enough CRT modulus.

Relation to Multivariate Factorization

Trager's norm algorithm can also be used for factoring multivariate polynomials over algebraic number fields (Wang EEZ + Zassenhaus recombination), but this is not yet implemented in oCAS. The current multivariate factorization (ocas-poly/src/factor/multivariate.rs) supports only $\mathbb{Z}$ and $\mathbb{F}_p$ coefficients.

Computational Complexity

For $f \in K[x]$ ($K = \mathbb{Q}(\alpha)$, $[K:\mathbb{Q}] = d$, $\deg f = n$):

StepComplexityDescription
Norm computation$O(dn \cdot (n + d^3))$ coefficient operationsper evaluation point: Horner evaluation $O(n)$ + resultant $O(d^3)$; $O(dn)$ points in total
Square-free factorization$O(n^2)$ GCD callsYun's algorithm; the bottleneck is the GCD
Modular GCD$O(d \cdot p \cdot \log p)$ per primeEuclidean GCD over $\mathrm{GF}(p^d)$
CRT combination$O(d \cdot n \cdot \log^2 M)$$M$ the CRT modulus, $n$ coefficients × $d$ $\alpha$-components

The overall bottleneck is usually the resultant evaluation in the norm computation (for large $d$) or the prime selection for the modular GCD (when many unlucky primes occur).


References

  • [Cohen] H. Cohen, A Course in Computational Algebraic Number Theory, Springer GTM 138, 1993. — Chapter 2 (representation and arithmetic of number fields), Chapter 3 (basic number-theoretic functions), Chapter 4 (ideals and class groups).
  • [Trager] B. M. Trager, "Algebraic factoring and rational function integration", Proc. SYMSAC 1976, pp. 196–208. — The original paper on Trager's norm algorithm.
  • [Encarnación] M. J. Encarnación, "Computing GCDs of polynomials over algebraic number fields", J. Symbolic Computation 20:3, 1995, pp. 299–313. — The modular GCD method over number fields.
  • [Gathen–Gerhard] J. von zur Gathen & J. Gerhard, Modern Computer Algebra, 3rd ed., Cambridge, 2013. — Chapter 6 (field extensions), Chapters 14–15 (factorization).
  • [Cox–Little–O'Shea] D. Cox, J. Little & D. O'Shea, Ideals, Varieties, and Algorithms, 4th ed., Springer UTM, 2015. — Chapter 4 (Gröbner bases over polynomial rings and elimination).
  • [Lang] S. Lang, Algebra, 3rd ed., Springer GTM 211, 2002. — Chapter 5 (basic theory of field extensions).

See also: Coefficient Domains API — the complete API of AlgebraicExtension, AlgebraicNumberField, AlgebraicElement; Factorization API — using DenseUnivariatePolynomial::factor() over algebraic number fields; Polynomial GCD & Factorization — the general theory of modular GCD and Hensel lifting; Finite Fields & Modular Arithmetic — the construction of $\mathbb{F}_p$ and $\mathrm{GF}(p^d)$.

Tensor Algebra & Canonicalization

This chapter discusses the algebraic structure of tensor expressions in oCAS and the mathematical foundations of canonicalization. The central question: given a tensor-product expression with contractions and symmetries, how can one decide whether two seemingly different writings (for example $T_{\mu\nu} U^{\nu\rho}$ and $U^{\rho\nu} T_{\nu\mu}$) represent the same mathematical object? oCAS's approach is to encode the tensor expression as a colored directed graph and then compute its canonical form via a graph-isomorphism algorithm.


Prerequisites

Tensor Products and Contractions

A tensor $T$ of type $(r, s)$ has $r$ contravariant indices (superscripts) and $s$ covariant indices (subscripts). The tensor product $T \otimes U$ of two tensors simply concatenates their indices.

Contraction pairs one contravariant index with one covariant index and sums over it. Under the Einstein summation convention:

$$ T_{\mu\nu} U^{\nu\rho} = \sum_{\nu} T_{\mu\nu} U^{\nu\rho} $$

Here $\nu$ occurs twice — once as a subscript and once as a superscript — and is therefore summed away; the result is a tensor of type $(1,1)$. The summed-away indices are called dummy indices and the remaining ones free indices.

Einstein summation vs. explicit matching in oCAS: the Einstein convention sums implicitly over repeated indices and relies on positions (upper/lower) to determine pairings. oCAS uses explicit matching — every index slot is an argument carrying a position marker, and a contraction is expressed by two slots sharing the same label. This removes the implicit dependence on "upper/lower" positions and makes the system more explicit.

Raising and Lowering Indices

In a space with a metric $g_{\mu\nu}$, indices can be raised and lowered by contracting with the metric:

$$ T^{\mu} = g^{\mu\nu} T_{\nu}, \qquad T_{\mu} = g_{\mu\nu} T^{\nu} $$

In the oCAS tensor system, index raising/lowering is marked on each index slot by the IndexPosition enum (Upper / Lower); no metric tensor is introduced automatically.


Basic Concepts

Index Matching

Each argument of a tensor function $T(i, j, k)$ is called an index slot. Every slot carries:

  • a label: an Atom, such as i, mu, d0
  • a variance: Upper (contravariant) or Lower (covariant)

A contraction between two slots is expressed by label matching: when two slots share the same label and one is Upper while the other is Lower, they form a contraction pair.

Dummy-Index Management

In a tensor product $T(i, j) \cdot U(j, k)$, the label $j$ occurs exactly twice and is therefore a dummy index. oCAS's dummy-index management follows these rules:

  1. Identification: walk the arguments of all Fun nodes in the expression and count the occurrences of every label. Labels occurring exactly 2 times are dummy indices.
  2. Validation: in canonicalization (canon.rs), a label occurring more than twice raises TensorCanonError::ContractedMoreThanOnce; dummy.rs declares the OverContracted and BadContraction (same-variance pair) error variants, but refresh_dummies itself does not perform a variance check.
  3. Renaming: after canonicalization, dummy indices are replaced by new names d0, d1, d2, … numbered per group. Different index groups (e.g. spacetime indices and internal indices) use different namespaces d{group}_{n}.
#![allow(unused)]
fn main() {
// Example of dummy-index renaming
// Input: T(i, j) * U(j, k) * V(i, l)
// Output: T(d0, d1) * U(d1, k) * V(d0, l)
// i → d0 (group 0, 0th), j → d1 (group 0, 1st)
}

This renaming is implemented by the refresh_dummies function in dummy.rs. It queries the index group to which a label belongs via TensorRegistry::index_group, ensuring that dummy indices of different dimensions use different namespaces.


Core Theory

Graph Encoding

The first step of tensor canonicalization is to encode the tensor expression as a colored directed graph $G = (V, E)$. The encoding rules are as follows (implemented by tensor_to_graph in canon.rs):

Vertex Types

TypeVertex data TgNodeMeaning
Head vertexHead(hash)a tensor function head $T$; hash is the hash of the function name
Slot vertexSlot(hash)an index slot; hash is the hash of the index label (symmetric slots uniformly use Slot(0))
Scalar vertexScalar(hash)a scalar factor in the product

Edge Types

TypeEdge data TgEdgeDirectionMeaning
Head→slotHeadToSlot(pos, flag)directedfrom a tensor head to its pos-th slot; flag=1 visible, flag=0 hidden (symmetric)
ContractionContraction(group)undirectedan edge between two contracted slot vertices; group identifies the index group

Handling Symmetries

When some slots of a tensor are symmetric (SymmetrySpec::symmetric_subsets), these slots are marked hidden in the graph:

  • The vertex data of symmetric slots is uniformly Slot(0), so the graph-isomorphism engine may freely permute them.
  • The head→slot edges of symmetric slots are marked HeadToSlot(pos, 0) (flag=0) and use the sorted position instead of the original one, so that $T(a, b)$ and $T(b, a)$ produce identical encodings (the flag itself still participates in the certificate comparison).
  • At input time, symmetric slots are pre-sorted by the alphabetical order of their labels, ensuring that $T(a, b)$ and $T(b, a)$ generate the same graph.

Key invariant: two tensor expressions are mathematically equivalent $\Leftrightarrow$ their graphs are isomorphic.

The McKay Refinement–Individualization Algorithm

Graph-isomorphism testing and canonical-labeling computation use the McKay refinement–individualization algorithm (of the nauty family), implemented in graph.rs (a standalone implementation of about 1100 lines).

Algorithm Overview

Given a graph $G$, the algorithm outputs:

  1. Canonical labeling $\lambda: V \to {0, 1, \dots, n-1}$ — a bijection such that for any isomorphic graph $G'$, the certificates produced by $\lambda$ and $\lambda'$ are identical.
  2. Automorphism orbits — the equivalence classes of vertices under the action of the automorphism group.
  3. Automorphism group order $|\text{Aut}(G)|$.

Step 1: Initial Coloring

Group the vertices by their data value: vertices with the same data belong to the same cell. For example, all Head(42) vertices are in one cell and all Slot(7) vertices in another.

This defines an ordered partition $\pi_0 = (C_1, C_2, \dots, C_k)$, where each $C_i$ is a set of vertices.

Step 2: 1-WL Color Refinement

The refinement process iteratively splits every non-trivial cell ($|C| > 1$) according to the neighbour signature of its vertices with respect to the other cells:

For the signature of a vertex $v \in C_i$ with respect to a target cell $C_j$:

$$ \sigma(v, C_j) = \text{sorted}\bigl[,(\text{edge_data}, \text{direction}) ;\big|; \text{edge } v \to w,; w \in C_j,\bigr] $$

where direction is encoded as: 0 = directed outgoing edge, 1 = directed incoming edge, 2 = undirected edge.

If vertices in the same cell have different signatures, the cell is split into several sub-cells by signature. This process repeats until the partition is equitable — i.e. for any two vertices $u, v$ in the same cell and any cell $C_j$, $\sigma(u, C_j) = \sigma(v, C_j)$.

Complexity: each refinement round produces at most $O(n)$ new cells, and each step takes $O(n^2)$ time to check all (cell, target) pairs. In practice, refinement typically converges within $O(n \log n)$ steps.

Step 3: The Individualization–Refinement Search (DFS)

If the partition is still non-discrete after refinement (some cell has $|C| > 1$), the algorithm enters the search phase:

  1. Choose the smallest non-trivial cell $C_{\min}$.
  2. Individualize each vertex $v$ of $C_{\min}$: move $v$ out of $C_{\min}$ into a new singleton cell (placed before $C_{\min}$). This breaks the symmetry between $v$ and the other vertices.
  3. Re-refine the partition after individualization.
  4. Recurse until the partition is discrete (every cell has exactly one vertex), producing a discrete labeling.

The search uses an iterative (stack-based) DFS; SearchFrame records the partition, the path invariant, and whether the node is on the leftmost path at each level.

Step 4: Path Invariants and Automorphism Pruning

Path Invariants

Every search node records a path invariant $\mathcal{I}$ — the sequence of cell sizes of the partitions along the path from the root to the current node:

$$ \mathcal{I} = \bigl[,|C_1^{(0)}|, \dots, |C_k^{(0)}|,\bigr] ;\to; \bigl[,|C_1^{(1)}|, \dots, |C_{k'}^{(1)}|,\bigr] ;\to; \cdots $$

If two search nodes have different path invariants, they cannot produce isomorphic discrete labelings, so they can be pruned safely. (The current implementation computes and stores this invariant in SearchFrame, but invariant-based pruning is not yet active — the only pruning actually performed is the automorphism-orbit pruning.)

Automorphism-Orbit Pruning

When the algorithm discovers that two different discrete labelings produce the same certificate, the mapping between them is an automorphism. This automorphism is recorded as an orbit generator.

In subsequent search, if the vertex $v$ to be individualized lies in the same orbit as an already processed vertex $w$ under the automorphism group (i.e. there is an automorphism mapping $v$ to $w$), then $v$ is skipped — because individualizing $v$ necessarily produces a subtree isomorphic to the one from individualizing $w$.

Orbits are computed by BFS closure: start from a seed vertex and repeatedly apply the generators (and their inverses) until closure.

Step 5: The Canonical Form

During the search, the algorithm maintains the lexicographically largest certificate found so far. Whenever a discrete node is reached, its certificate is computed:

$$ \text{cert}(\lambda) = \text{sorted}\bigl[,(i, j, \text{edge_data}, \text{dir}) ;\big|; \text{edge } \lambda^{-1}(i) \to \lambda^{-1}(j),\bigr] $$

If the new certificate is larger, the best labeling is updated; if it is equal, an automorphism generator is recorded.

Final output:

  • Canonical graph: the graph relabeled by the best labeling.
  • Automorphism group order: computed by enumerating all permutations generated by the generators via BFS closure (bounded by $n!$, in practice far smaller).
  • Orbit partition: computed from the generator set by compute_orbits.

A Complete Example

Consider the expression $T_{\mu\nu} U^{\nu\rho}$, where $T$ and $U$ have no symmetries:

Graph encoding:

Vertices: H_T (Head), S_mu (Slot), S_nu_T (Slot), H_U (Head), S_nu_U (Slot), S_rho (Slot)
Edges:
  H_T → S_mu    (HeadToSlot(0, 1))
  H_T → S_nu_T  (HeadToSlot(1, 1))
  H_U → S_nu_U  (HeadToSlot(0, 1))
  H_U → S_rho   (HeadToSlot(1, 1))
  S_nu_T — S_nu_U (Contraction(0))   // undirected

Initial coloring: vertices are grouped by their data — {H_T} and {H_U} form two separate Head cells (different hashes of the function names); slot vertices are grouped by the hash of their labels: {S_mu}, {S_nu_T, S_nu_U} (both labelled nu), {S_rho}.

Refinement: the non-trivial cell {S_nu_T, S_nu_U} has different neighbour signatures with respect to the target cell {H_T} ($S_{\nu_T}$ has an incoming edge from $H_T$ — the head-to-slot edge runs from $H_T$ to $S_{\nu_T}$ — while $S_{\nu_U}$ does not), so it is split.

Result: after refinement a discrete partition is reached and no search is needed. The canonical form determines the order of the tensor factors and the naming of the dummy indices.


Young Tableaux and Symmetrization Projectors

Young tableaux are the classical tool for representing the irreducible representations of the symmetric group; in tensor algebra they construct tensor components with prescribed symmetries.

Definition of a Young Tableau

A Young tableau is defined by an integer partition $\lambda = (\lambda_1 \geq \lambda_2 \geq \cdots \geq \lambda_k > 0)$, where $\lambda_i$ is the number of boxes in the $i$-th row. The total number of boxes $|\lambda| = \sum \lambda_i$ equals the rank of the tensor.

For example, the Young diagram for $\lambda = (2, 1)$:

□ □
□

The Young Symmetrizer

Given a Young tableau $\lambda$, its Young symmetrizer $e_\lambda$ acts on a tensor $T$ to produce the component with the symmetry prescribed by $\lambda$. It is defined as follows:

  1. Row symmetrization: fully symmetrize the indices within each row.
  2. Column antisymmetrization: fully antisymmetrize the indices within each column.
  3. Composition: antisymmetrize over columns first, then symmetrize over rows.

Formally, letting $R$ be the direct product of the row symmetry groups and $C$ the direct product of the column antisymmetry groups:

$$ e_\lambda = \left(\sum_{\sigma \in R} \sigma\right) \cdot \left(\sum_{\tau \in C} \operatorname{sgn}(\tau) \cdot \tau\right) $$

The projection onto a tensor $T$ is:

$$ (e_\lambda \cdot T){i_1 \dots i_n} = \sum{\sigma \in R} \sum_{\tau \in C} \operatorname{sgn}(\tau) \cdot T_{i_{\sigma(\tau(1))} \dots i_{\sigma(\tau(n))}} $$

Computing the Sign of a Permutation

For a given permutation $\pi$, the sign computation of the Young projection proceeds in two steps:

  1. Row-constraint check: for each position $p$, check whether $\pi(p)$ lies in the same row as $p$. If not (and the row length > 1), then $\operatorname{sgn} = 0$ (the permutation does not preserve the shape of the tableau).

  2. Column parity: for each column $c$, extract the sub-permutation of $\pi$ restricted to the positions of that column and compute its parity (via cycle decomposition: a cycle of even length contributes a factor $-1$). The parities of all columns multiply to the total sign.

$$ \operatorname{sgn}\lambda(\pi) = \begin{cases} \displaystyle\prod{c} (-1)^{#\text{even cycles in column } c} & \text{if } \pi \text{ preserves rows} \ 0 & \text{otherwise} \end{cases} $$

The oCAS Implementation: Explicit Expansion

The implementation in young.rs uses an explicit permutation expansion (not a BSGS group-theoretic method):

  1. Expand the row group $R$ (permutations within each row) and the column group $C$ (permutations within each column) into full position maps, for a total of $\prod r_i! \cdot \prod c_j!$ combinations.
  2. For each combination $(\sigma, \tau)$, form the composed permutation $\tau \circ \sigma$; the term coefficient is the column permutation parity $\operatorname{sgn}(\tau)$.
  3. Each term contributes $\operatorname{sgn}(\tau) \cdot T(i_{\tau\sigma(1)}, \dots, i_{\tau\sigma(n)})$.
  4. Sum all terms — this is the classical Young symmetrizer $c_\lambda = a_\lambda b_\lambda$, with no normalization by the product of hook lengths; every term has coefficient $\pm 1$ only. The result is therefore a projector up to the scalar $\kappa = (\prod r_i! \cdot \prod c_j!) / \dim(\lambda)$ ($c_\lambda^2 = \kappa \cdot c_\lambda$).

Fully antisymmetric tableau $\lambda = (1, 1, \dots, 1)$: the row group is trivial and the column group is the whole $S_n$, producing the standard alternating sum.

Fully symmetric tableau $\lambda = (n)$: one element per column, so column antisymmetrization degenerates to the identity and row symmetrization produces the sum over all permutations.


Symmetry Specifications

The SymmetrySpec struct declares the slot symmetries of a tensor function head; it is the bridge between graph encoding and Young projection.

Three Types of Symmetry

TypeFieldGraph-encoding behaviorYoung-projection behavior
Symmetric subsetssymmetric_subsetsslots are marked hidden (is_slot_hidden = true); the graph isomorphism may permute them freelypreserves the corresponding row symmetry
Antisymmetric subsetsantisymmetric_subsetsslots are visible and encoded exactly like ordinary slots (no special marker; the antisymmetry signs are handled at the expression level by the Young projection)preserves the corresponding column antisymmetry
Cyclic permutationscyclicslots are marked hiddensymmetrization over cyclic permutations

The is_slot_hidden Test

A slot $p$ is marked hidden if and only if:

$$ \text{is_hidden}(p) \iff \exists S \in \text{symmetric_subsets}: p \in S ;\lor; \exists C = \text{cyclic}: p \in C $$

Hidden slots use the uniform color Slot(0) in the graph, letting the graph-isomorphism engine permute them freely, which guarantees $T(a, b) = T(b, a)$ for symmetric tensors.

TensorRegistry

TensorRegistry manages the specifications and index groups of all tensors:

  • register(name, spec): register a tensor function head together with its symmetry specification.
  • set_index_group(label, group): set the group to which an index label belongs (e.g. spacetime indices group 1, internal indices group 2).
  • index_group(label): query the index group (0 = default/unassigned).

The role of index groups is to prevent cross-dimension conflicts when renaming dummy indices. For example, a spacetime dummy index $\mu$ and an internal dummy index $i$ use different namespaces d1_0, d1_1, … and d2_0, d2_1, ….


Implementation in oCAS

Module Structure

ocas-atom/src/tensor/
├── canon.rs      # graph encoding + canonicalization entry
├── graph.rs      # McKay refinement-individualization engine (about 1100 lines)
├── young.rs      # Young tableau projection
├── spec.rs       # SymmetrySpec + TensorRegistry
└── dummy.rs      # dummy-index management

canon.rs: Graph Encoding and Canonicalization

Entry function canonicalize_tensors(ctx, expr, registry):

  1. If the expression is a sum (Add), canonicalize each term and verify that the free indices are consistent.
  2. A monomial goes to canonicalize_single_term:
    • Fast path: if the tensor has symmetric subsets, no antisymmetric subsets, and all slots are hidden (is_slot_hidden is true for all), simply sort the arguments by the alphabetical order of their labels; no graph isomorphism is needed.
    • General path: encode the graph with tensor_to_graph, compute the canonical form with g.canonize(), and rebuild the expression with reconstruct.

Encoding logic of tensor_to_graph:

  • For each factor Fun(name, args) in the product:
    • create a head vertex Head(hash(name)).
    • pre-sort the symmetric slots by the alphabetical order of their labels, so that $T(a,b)$ and $T(b,a)$ generate the same graph.
    • create a slot vertex for each argument; hidden slots use Slot(0) and visible slots use Slot(hash(label)).
    • add the directed edge Head → Slot.
  • For pairs of slots whose label occurs exactly twice in the product, add the undirected contraction edge.

Rebuilding logic of reconstruct:

  • Walk the head vertices of the canonical graph and collect the tensor factors in canonical order.
  • For each head's outgoing edges (sorted by orig_pos or label order), determine the slot labels:
    • contracted slots: assign the canonical dummy names d0, d1, …
    • free slots: keep the original labels.
  • Assemble the factors into a product or a sum expression.

graph.rs: The McKay Algorithm

This is the largest and most self-contained algorithmic module in oCAS (about 1100 lines), implementing a complete nauty-style graph-isomorphism engine.

Data structures:

  • Graph<N, H, E>: a three-parameter generic graph. N = visible vertex data (participates in comparison), H = hidden vertex data (does not participate), E = edge data (participates).
  • Partition: an ordered partition; cells[i] is the vertex list of the $i$-th cell and cell_of[v] is the index of the cell containing vertex $v$.
  • SearchFrame: a DFS search stack frame holding the partition, the path invariant, and whether the node is on the leftmost path.
  • CanonicalForm<N, H, E>: the output result, containing vertex_map (original → canonical mapping), orbits (automorphism orbits), automorphism_group_size (group order), and graph (the canonical graph).

Key implementation details:

  • Refinement (Partition::refine): uses the stable_below pointer optimization — cells that are already stable no longer participate in split checks. When a cell is split, stable_below retreats to that cell's index, ensuring subsequent refinement covers all affected cells.
  • Neighbour signatures (cell_signatures / vertex_signature): for each vertex $v$, collect the (edge_data, direction) pairs to all neighbours in the target cell $C_j$, and sort them into a signature. direction distinguishes directed outgoing (0), directed incoming (1), and undirected (2) edges.
  • Certificate (Graph::certificate): convert all edges of the graph into a list of (i, j, data, dir) quadruples under the canonical labeling, sorted to serve as the key for lexicographic comparison.
  • Automorphism group order (group_size): enumerates all permutations generated by the generators via BFS closure (full enumeration, no truncation; bounded by $n!$, in practice far smaller).
  • Orbit computation (compute_orbits): BFS starts from each unvisited vertex and expands along the generators and their inverses until closure.

young.rs: Young Projection

The YoungTableau struct:

  • row_lengths: Vec<usize>: the number of boxes in each row; e.g. [2, 1] denotes $\lambda = (2,1)$.
  • total_boxes(): the total number of boxes.
  • sign_of_permutation(perm): compute the sign of a permutation under the given tableau ($-1, 0, +1$).

young_project(ctx, tensor_expr, tableau):

  • only handles expressions of the form Fun(name, args).
  • generates all permutations with Heap's algorithm ($O(n!)$ time, constant space).
  • computes the sign of each permutation and, when non-zero, constructs the signed term.
  • returns the sum of all terms (or a single term, or zero).

Note: this is an explicit expansion, suitable for low-rank tensors ($n \lesssim 8$). For high-rank tensors, a BSGS (basic Schreier–Sims) method should be considered, but it is not currently implemented in oCAS.

Known limitation: the column-parity code of sign_of_permutation panics (via unwrap) on permutations that preserve rows but move elements across columns (the column-position lookup for the element's new position fails). The fully symmetric tableau $(n)$ and the fully antisymmetric tableau $(1, \ldots, 1)$ never trigger this; general shapes (such as $(2,1)$) can panic during the explicit expansion, so care is needed.

spec.rs: Symmetry Specifications

The three constructors of SymmetrySpec:

  • none(): no symmetry.
  • fully_symmetric(rank): all slots fully symmetric.
  • fully_antisymmetric(rank): all slots fully antisymmetric.

is_slot_hidden(pos): checks whether a slot belongs to a symmetric subset or a cyclic subset. Hidden slots do not participate in the graph-canonicalization comparison, allowing the graph-isomorphism engine to permute them freely.

dummy.rs: Dummy-Index Management

refresh_dummies(ctx, expr, registry):

  1. Collect the occurrence counts of all Fun node arguments in the expression.
  2. Identify the dummy indices (occurring exactly twice).
  3. For each dummy index, query its index group (registry.index_group) and assign a number within the group.
  4. Rename: dummy indices of group 0 are named d0, d1, …; those of group $g$ are named d{g}_0, d{g}_1, ….

Error variants (declared in dummy.rs, but not raised by refresh_dummies):

  • OverContracted(Symbol): an index occurs more than twice.
  • BadContraction(Symbol): the two occurrences have the same variance (not an upper/lower pair).

(In the canonicalization path, a label occurring more than twice raises TensorCanonError::ContractedMoreThanOnce from canon.rs.)


Advanced Topics

Hidden Slots and the Fast Path

For fully symmetric tensors (such as the metric $g_{\mu\nu}$), all slots are marked hidden. All slot vertices in the graph then share the same color Slot(0), and the automorphism group of the McKay algorithm becomes very large ($|S_n| = n!$).

To avoid this cost, canonicalize_single_term takes the fast path when it detects a fully symmetric tensor: sort the arguments directly by the alphabetical order of their labels without building a graph. This reduces the canonicalization complexity from $O(n!)$ to $O(n \log n)$.

Uses of Automorphism-Group Information

The McKay algorithm outputs not only the canonical form, but also the orbits and the order of the automorphism group. This information has important applications in tensor algebra:

  • Orbits: identify which slots are equivalent under the symmetries, useful for simplifying sums.
  • Group order: $|\text{Aut}(G)|$ is a measure of the intrinsic symmetry of the expression, useful for estimating the size of a symmetrization expansion.

Relation to nauty

graph.rs is a standalone nauty-style implementation with no external dependencies. It implements the core components of McKay's algorithm (1981/2014):

  • 1-WL color refinement (equivalent to nauty's refine)
  • individualization–refinement DFS (equivalent to search)
  • path-invariant recording (equivalent to nauty's compare; pruning based on it is not yet active)
  • automorphism-orbit pruning (equivalent to nauty's orbits)

The main differences from the original nauty:

  • uses adjacency lists instead of bit matrices, suitable for sparse graphs.
  • the certificate uses a sorted edge list instead of a canonical adjacency matrix.
  • the hidden data (the H parameter) allows storing metadata that does not participate in comparison.

Extending to Higher Symmetries

For more complex symmetries (such as the algebraic Bianchi identities of the Riemann curvature tensor $R_{abcd}$), the current system approximates them by combining symmetric_subsets and antisymmetric_subsets. Full Young symmetrization is provided by the young_project function, but it produces an expanded sum rather than a compact representation.


References

  1. Cvitanović, P. Group Theory: Birdtracks, Lie's, and Exceptional Groups. Princeton University Press, 2008. — The classic reference on tensor invariants and representations of symmetry groups.
  2. McKay, B. D. Practical Graph Isomorphism. Congressus Numerantium, 30:45–87, 1981 (updated 2014). — The original paper on the nauty algorithm.
  3. Faugère, J.-C. A new efficient algorithm for computing Gröbner bases (F4). Journal of Pure and Applied Algebra, 139(1–3):61–88, 1999. — The F4 algorithm, related to the matrix method for tensor contractions.
  4. Cox, D., Little, J., O'Shea, D. Ideals, Varieties, and Algorithms. Springer, 4th ed., 2015. — Polynomial-algebra foundations; reference for Gröbner basis theory.
  5. Hamermesh, M. Group Theory and Its Application to Physical Problems. Dover, 1989. — Physical applications of Young tableaux and tensor symmetries.

Monte Carlo Integration

Prerequisites

  • Linear Algebra — matrix operations (for understanding high-dimensional sampling)
  • Basic probability theory — random variables, expectation, variance
  • Basic calculus — the definition of the definite integral and change of variables

The following probability-theory background is recommended:

ConceptDescription
Expectation $\mathbb{E}[X]$the weighted average of a random variable
Variance $\text{Var}(X) = \mathbb{E}[(X - \mu)^2]$a measure of how far a random variable deviates from its mean
Independent and identically distributed (i.i.d.)several random variables that are mutually independent and identically distributed
Law of large numbersthe sample mean converges to the expectation as the sample size grows
Central limit theoremthe sample mean is approximately normally distributed

Basic Concepts

From Definite Integrals to Expectations

Consider the definite integral

$$I = \int_\Omega f(\mathbf{x}), d\mathbf{x}$$

where $\Omega \subseteq \mathbb{R}^d$ is the integration domain. The core idea of Monte Carlo methods is to rewrite the integral as an expectation.

Let $\mathbf{X}$ be a random variable uniformly distributed over $\Omega$, with probability density function

$$p(\mathbf{x}) = \frac{1}{V}, \qquad V = \text{vol}(\Omega)$$

Then the integral becomes

$$I = V \cdot \mathbb{E}[f(\mathbf{X})]$$

The Basic Estimator

The basic Monte Carlo estimator: draw $N$ independent samples $\mathbf{x}_1, \dots, \mathbf{x}_N$ from $p(\mathbf{x})$ and define

$$\hat{I} = \frac{V}{N} \sum_{i=1}^{N} f(\mathbf{x}_i)$$

Properties:

  1. Unbiasedness: $\mathbb{E}[\hat{I}] = I$ (its expectation equals the true value exactly).
  2. Variance: $\text{Var}(\hat{I}) = \frac{V^2}{N} \text{Var}(f(\mathbf{X}))$.
  3. Convergence rate: the standard error of $\hat{I}$ is $\sigma_{\hat{I}} \propto 1/\sqrt{N}$, independent of the dimension $d$.

The last property is the core advantage of Monte Carlo methods: the error of classical numerical integration (trapezoidal rule, Simpson's rule) grows exponentially with the dimension (the "curse of dimensionality"), whereas the convergence rate of Monte Carlo is always $O(N^{-1/2})$.

The Law of Large Numbers and the Central Limit Theorem

Law of large numbers (Kolmogorov's strong law). If $X_1, X_2, \dots$ are independent identically distributed random variables with $\mathbb{E}[|X_1|] < \infty$, then

$$\bar{X}n = \frac{1}{n}\sum{i=1}^{n} X_i ;\xrightarrow{\text{a.s.}}; \mathbb{E}[X_1]$$

Applied to Monte Carlo: $\hat{I}/V \to \mathbb{E}[f(\mathbf{X})]$ holds almost surely, hence $\hat{I} \to I$.

Central limit theorem (CLT). Under the above conditions, if $\text{Var}(X_1) = \sigma^2 < \infty$, then

$$\frac{\bar{X}_n - \mu}{\sigma / \sqrt{n}} ;\xrightarrow{d}; \mathcal{N}(0, 1)$$

Applied to Monte Carlo: for sufficiently large $N$, $\hat{I}$ is approximately normally distributed:

$$\hat{I} ;\sim; \mathcal{N}!\left(I,; \frac{V^2 \sigma_f^2}{N}\right)$$

where $\sigma_f^2 = \text{Var}(f(\mathbf{X}))$. This provides the basis for confidence intervals.

Variance Estimation and Confidence Intervals

In practice $\sigma_f^2$ is unknown and is estimated by the sample variance:

$$\hat{\sigma}f^2 = \frac{1}{N-1} \sum{i=1}^{N} \bigl(f(\mathbf{x}i) - \bar{f}\bigr)^2, \qquad \bar{f} = \frac{1}{N}\sum{i=1}^{N} f(\mathbf{x}_i)$$

The standard error of $\hat{I}$ is estimated as

$$\hat{\sigma}_{\hat{I}} = \frac{V}{\sqrt{N}} \hat{\sigma}_f$$

The $95%$ confidence interval is $\hat{I} \pm 1.96, \hat{\sigma}_{\hat{I}}$.

Generalization to weighted sampling: when the sampling density $p(\mathbf{x})$ is not uniform (importance sampling), each sample $i$ carries the weight $w_i = 1/p(\mathbf{x}_i)$, and

$$\hat{I} = \frac{1}{N} \sum_{i=1}^{N} w_i \cdot f(\mathbf{x}_i)$$

the variance estimate becomes

$$\hat{\sigma}^2 = \frac{1}{N} \sum_{i=1}^{N} w_i^2 f(\mathbf{x}i)^2 - \left(\frac{1}{N}\sum{i=1}^{N} w_i f(\mathbf{x}_i)\right)^2$$

oCAS's StatisticsAccumulator uses a similar weighted-variance form (see the "Statistics Accumulator" section below).


Core Theory

Importance Sampling

The variance of a Monte Carlo estimator depends on how much the integrand "fluctuates" under the sampling distribution. Importance sampling reduces the variance by choosing a non-uniform sampling distribution $p(\mathbf{x})$.

Principle: rewrite the integral as

$$I = \int_\Omega \frac{f(\mathbf{x})}{p(\mathbf{x})} \cdot p(\mathbf{x}), d\mathbf{x} = \mathbb{E}_p!\left[\frac{f(\mathbf{X})}{p(\mathbf{X})}\right]$$

The estimator is

$$\hat{I}p = \frac{1}{N} \sum{i=1}^{N} \frac{f(\mathbf{x}_i)}{p(\mathbf{x}_i)}, \qquad \mathbf{x}_i \sim p$$

Optimal sampling distribution: the variance-minimizing $p^*$ is

$$p^*(\mathbf{x}) = \frac{|f(\mathbf{x})|}{\int_\Omega |f(\mathbf{y})|, d\mathbf{y}}$$

when $f$ does not change sign, $w \cdot f = \int |f| \cdot \mathrm{sgn}(f)$ is constant and the variance is zero; if $f$ changes sign the variance is still minimized. But $p^$ itself requires knowing the integral — a chicken-and-egg problem. In practice, an adaptive grid is used to approximate $p^$.

Key constraint: $p(\mathbf{x})$ must be strictly positive at every point where $f(\mathbf{x}) \neq 0$. If $p$ vanishes where $f$ is non-zero, the estimator is unbounded and the variance is infinite.

The Vegas Adaptive Grid

The Vegas algorithm (Lepage, 1978) is the most widely used adaptive Monte Carlo integration method. Its core idea is to approximate the optimal distribution $p^*$ by a piecewise-constant sampling density.

The Product-Grid Structure

Vegas works on the $d$-dimensional unit hypercube $[0,1]^d$ (the physical domain is mapped to the hypercube by a linear change of variables). The sampling density has a product form:

$$p(\mathbf{x}) = \prod_{k=1}^{d} p_k(x_k)$$

Each $p_k$ is an independent one-dimensional piecewise-constant density on the $k$-th axis. This means the grid of each dimension is maintained independently — the $d$-dimensional problem is decomposed into $d$ one-dimensional problems.

The grid of the $k$-th dimension: divide $[0,1]$ into $M$ bins with boundaries

$$0 = b_0 < b_1 < \cdots < b_M = 1$$

The width of the $j$-th bin is $\Delta_j = b_{j+1} - b_j$. On that bin $p_k(x) = 1/(M \cdot \Delta_j)$ (constant), because each bin is chosen with equal probability.

The Sampling Procedure

For each sample point $\mathbf{x} = (x_1, \dots, x_d)$:

  1. Pick a bin: for the $k$-th dimension, choose the bin number $j_k$ uniformly at random from ${0, 1, \dots, M-1}$.
  2. Pick a point: choose $x_k$ uniformly at random within $[b_{j_k}, b_{j_k+1}]$.
  3. Compute the Jacobian (weight): the probability density at this point is $p_k(x_k) = 1/(M \cdot \Delta_{j_k})$, hence

$$w_k = \frac{1}{p_k(x_k)} = M \cdot \Delta_{j_k}$$

The total weight is

$$w = \prod_{k=1}^{d} w_k = \prod_{k=1}^{d} (M \cdot \Delta_{j_k})$$

  1. Accumulate: add $w \cdot f(\mathbf{x})$ to the statistics accumulator.

Physical meaning of the weight: $w = 1/p(\mathbf{x})$ is the reciprocal of the sampling density — samples in sparse regions (large $\Delta_j$) get larger weights and samples in dense regions (small $\Delta_j$) get smaller weights, compensating for the non-uniform sampling.

Grid Adaptation: The Training Signal

In each iteration, Vegas records the "importance" of every bin. For the $j$-th bin of the $k$-th dimension, the training signal is

$$d_j = \sum_{\text{samples in bin } j} w \cdot f^2(\mathbf{x})$$

Intuition: $f^2$ measures the "energy" (variance contribution) of the integrand in the region of the bin, and $w$ compensates for non-uniform sampling. Bins with high $d_j$ contribute heavily to the integration error and need a finer grid.

Grid Updating: Smoothed Cumulative-Arc-Length Redistribution

Given the training signals ${d_0, d_1, \dots, d_{M-1}}$, Vegas updates the grid boundaries through the following steps:

Step 1: 3-bin smoothing. For each bin $j$, compute the smoothed value:

$$\tilde{d}j = \frac{d{j-1} + d_j + d_{j+1}}{3}$$

Missing neighbours of boundary bins ($j = 0$ or $j = M-1$) are replaced by 0. The purpose of smoothing is to suppress the noise of individual bins and make the grid changes more stable.

Step 2: mean normalization. Compute the average $\bar{d} = \frac{1}{M}\sum_j \tilde{d}_j$ and set

$$\hat{d}_j = \frac{\tilde{d}_j}{\bar{d}}$$

After normalization, $\hat{d}_j = 1$ means "average importance" and $\hat{d}_j > 1$ means the bin needs a finer grid.

Step 3: damping. Introduce a learning rate $\alpha > 0$ (default $\alpha = 1.5$ in oCAS) and apply a power transform to the normalized values:

$$\hat{d}_j ;\leftarrow; \hat{d}_j^{1/\alpha}$$

When $\alpha = 1$ there is no damping ($\hat{d}_j^{1/1} = \hat{d}_j$); when $\alpha > 1$ the exponent is $< 1$, compressing extreme values and making grid updates more conservative. This prevents the grid from overreacting to the noise of a single iteration.

Step 4: cumulative arc length and equidistant redistribution. Compute the cumulative sums:

$$S_j = \sum_{i=0}^{j-1} \hat{d}_i, \qquad j = 0, 1, \dots, M$$

where $S_0 = 0$ and $S_M = \sum_{i=0}^{M-1} \hat{d}_i$. Treat $S$ as an "arc-length" curve; the new bin boundaries are equidistributed along the arc length:

$$b'_j = \text{the position in } [0,1] \text{ corresponding to the arc length } S = j \cdot S_M / M$$

Concretely, for $j = 1, \dots, M-1$, find the index $i$ in $S$ with $S_i \leq j \cdot S_M / M < S_{i+1}$ and then interpolate linearly:

$$b'j = \frac{i + \frac{j \cdot S_M / M - S_i}{S{i+1} - S_i}}{M}$$

Fix $b'_0 = 0$ and $b'_M = 1$.

Step 5: monotonicity clamping. Due to floating-point precision and numerical noise, the $b'_j$ may not be strictly increasing. Repair the monotonicity of the result:

$$b'_j ;\leftarrow; \max(b'j,; b'{j-1})$$

Intuition: the geometric meaning of arc-length redistribution is that — if we plot the cumulative-importance curve in the $(x, S(x))$ plane, the "equal-arc-length division points" make every new bin sweep equal arc length on the curve, i.e. every new bin carries approximately equal integration contribution.

Stratified Sampling

Stratified sampling is another variance-reduction technique. Its idea is to divide the integration domain into disjoint subdomains ("strata") and sample independently within each stratum:

$$I = \sum_{j=1}^{M} I_j, \qquad I_j = \int_{\Omega_j} f(\mathbf{x}), d\mathbf{x}$$

The estimator of each stratum is $\hat{I}_j = V_j \cdot \bar{f}_j$ ($V_j$ the stratum volume, $\bar{f}_j$ the within-stratum sample mean), and the total estimator is $\hat{I} = \sum_j \hat{I}_j$.

Variance analysis:

$$\text{Var}(\hat{I}) = \sum_{j=1}^{M} \frac{V_j^2}{N_j} \sigma_j^2$$

where $N_j$ is the number of samples in the $j$-th stratum and $\sigma_j^2$ is the variance of $f$ within the $j$-th stratum. If the stratum variances differ, the optimal allocation (Neyman allocation) is

$$N_j \propto V_j \sigma_j$$

i.e. strata with higher variance receive more samples.

The relation between Vegas and stratified sampling: the product grid of Vegas is essentially an adaptive stratified sampling — the bin boundaries stratify the integration domain and sampling is uniform within each stratum. The difference is that Vegas's stratum boundaries are adapted iteratively rather than fixed in advance.

Inverse-Variance Weighting Across Iterations

Vegas runs several iterations (with $N$ samples each); every iteration produces an estimate $\hat{I}_r$ and its standard error $\hat{\sigma}_r$. How should the iteration results be combined?

Inverse-variance weighting: let $w_r = 1/\hat{\sigma}_r^2$; the combined estimate is

$$\hat{I}_{\text{combined}} = \frac{\sum_r w_r \hat{I}_r}{\sum_r w_r}$$

and the combined error is

$$\hat{\sigma}_{\text{combined}} = \frac{1}{\sqrt{\sum_r w_r}}$$

Optimality: when the estimates of different iterations are mutually independent, inverse-variance weighting is the minimum-variance unbiased way of combining them (a direct corollary of the Gauss–Markov theorem).

Online inverse-variance accumulation: to avoid storing the estimates of all iterations, StatisticsAccumulator uses online updates (a weighted mean plus an incremental $\chi^2$; this is not Welford's algorithm itself). It maintains the state variables:

  • integral: the current combined estimate
  • error: the current combined standard error
  • chi_square: the across-iteration $\chi^2$ statistic

When iteration $r$ completes (estimate $\mu_r$, error $\sigma_r$), the update rules are:

$$w_{\text{prev}} = 1/\sigma_{\text{prev}}^2, \quad w_r = 1/\sigma_r^2, \quad w_{\text{new}} = w_{\text{prev}} + w_r$$

$$\hat{I}{\text{new}} = \frac{w{\text{prev}} \cdot \hat{I}{\text{prev}} + w_r \cdot \mu_r}{w{\text{new}}}$$

$$\sigma_{\text{new}} = 1/\sqrt{w_{\text{new}}}$$

Implementation detail: to prevent zero-variance iterations from overflowing the weights, oCAS clamps the error at $\sigma_r \geq 10^{-150}$ ($10^{-300}$ is still representable as an f64).

The $\chi^2$ Diagnostic

Purpose: to test whether the estimates of the different iterations are consistent — if the $\hat{I}_r$ differ far more than their error bars, the variance is underestimated or the integrator has not converged.

Definition:

$$\chi^2 = \sum_{r=1}^{R} w_r (\hat{I}r - \hat{I}{\text{combined}})^2$$

where $w_r = 1/\hat{\sigma}_r^2$.

Interpretation: if the iterations are independent and the variance estimates are accurate, $\chi^2$ follows a chi-square distribution with $R - 1$ degrees of freedom. The empirical rule is:

$\chi^2 / (R-1)$Meaning
$\approx 1$Normal — the iterations are consistent
$\gg 1$The variance is underestimated, or the integrator has not converged
$\ll 1$The variance is overestimated (rare)

oCAS's StatisticsAccumulator updates $\chi^2$ online at the end of each iteration:

$$\chi^2_{\text{new}} = \chi^2_{\text{prev}} + w_{\text{prev}} (\hat{I}{\text{prev}} - \hat{I}{\text{new}})^2 + w_r (\mu_r - \hat{I}_{\text{new}})^2$$

Note this is an incremental update — the estimates of past iterations need not be stored.


Implementation in oCAS

oCAS's Vegas implementation lives in ocas-eval/src/numeric/vegas.rs; the accompanying statistics accumulator is in ocas-eval/src/numeric/statistics.rs.

Data Structures

Vegas
├── opts: VegasOptions        // tunable options
├── axes: Vec<GridAxis>       // one independent grid per dimension
│   ├── boundaries: Vec<f64>  // M+1 bin boundaries ∈ [0,1]
│   └── bin_accum: Vec<f64>   // training-signal accumulator for the M bins
└── accumulator: StatisticsAccumulator  // inverse-variance weighting across iterations
    ├── sum_w, sum_wf, sum_wf2  // weighted sums for the current iteration
    ├── integral, error          // combined estimate and error
    ├── chi_square               // χ² diagnostic
    └── iterations               // number of completed iterations

Default Parameters

ParameterDefaultMeaning
n_bins64number of bins per dimension $M$
n_samples10,000number of samples per iteration $N$
iterations10number of adaptive iterations $R$
learning_rate1.5grid-update damping parameter $\alpha$
seed0x0C45deterministic RNG seed

Implementing the Sampling Procedure

Pseudocode of one iteration:

#![allow(unused)]
fn main() {
for _ in 0..n_samples {
    // 1. sample each dimension independently
    let mut x = Vec::with_capacity(n_dims);
    let mut jac = 1.0;
    for axis in axes.iter_mut() {
        // pick a bin uniformly
        let b = rng.random_range(0..n_bins);
        let lo = boundaries[b];
        let hi = boundaries[b + 1];
        // sample uniformly within the bin
        let u = rng.random::<f64>();
        let xi = lo + (hi - lo) * u;
        // Jacobian = M × bin width = 1/pdf
        let wi = (hi - lo) * n_bins as f64;
        x.push(xi);
        jac *= wi;
    }
    // 2. evaluate and accumulate
    let fx = f(&x);
    accumulator.add_sample(jac, fx);
    // 3. record the training signal
    for (i, xi) in x.iter().enumerate() {
        axes[i].add_training(*xi, jac, fx * fx);
    }
}
}

Key implementation details:

  • RNG choice: uses Xoshiro256PlusPlus (rather than thread_rng) with a fixed seed to guarantee reproducible results.
  • Product Jacobian: the total weight is jac = ∏ w_k — the dimensions are sampled independently, so the total density is the product of the per-dimension densities.
  • Training signal: add_training locates the bin by binary search ($O(\log M)$) and accumulates weight × f² into the corresponding bin.

Implementing the Grid Update

The implementation steps of GridAxis::update(learning_rate):

#![allow(unused)]
fn main() {
fn update(&mut self, learning_rate: f64) {
    let n = self.bin_accum.len();
    let total: f64 = self.bin_accum.iter().sum();
    if total <= 0.0 { return; }

    // 1. 3-bin smoothing
    let avg = total / n as f64;
    let mut d = vec![0.0; n];
    for i in 0..n {
        let prev = if i > 0 { bin_accum[i-1] } else { 0.0 };
        let next = if i+1 < n { bin_accum[i+1] } else { 0.0 };
        d[i] = (prev + bin_accum[i] + next) / 3.0 / avg;
    }

    // 2. damping: d[i] = d[i]^(1/learning_rate)
    if learning_rate != 1.0 {
        for v in d.iter_mut() {
            *v = v.max(1e-30).powf(1.0 / learning_rate);
        }
    }

    // 3. cumulative arc length
    let mut cum = vec![0.0; n+1];
    for i in 0..n { cum[i+1] = cum[i] + d[i]; }

    // 4. equidistant redistribution
    let mut new_boundaries = vec![0.0; n+1];
    new_boundaries[0] = 0.0;
    new_boundaries[n] = 1.0;
    let mut j = 0;
    for i in 1..n {
        let target = i as f64 / n as f64 * cum[n];
        while j < n && cum[j+1] < target { j += 1; }
        let frac = (target - cum[j]) / (cum[j+1] - cum[j]);
        new_boundaries[i] = (j as f64 + frac) / n as f64;
    }

    // 5. monotonicity clamping
    for i in 1..=n {
        if new_boundaries[i] < new_boundaries[i-1] {
            new_boundaries[i] = new_boundaries[i-1];
        }
    }
    new_boundaries[n] = 1.0;
    self.boundaries = new_boundaries;
    self.bin_accum.fill(0.0);  // reset the training signal
}
}

Numerical safeguards in the implementation:

  • the normalised values take max(1e-30) before the power transform, guaranteeing $d_i > 0$ so the cumulative arc length is strictly increasing (preventing division by zero in the interpolation).
  • when learning_rate = 1.0, the power transform (together with the 1e-30 clamp) is skipped, so cum[j+1] - cum[j] can still be zero (a bin completely flat), in which case frac = 0.
  • the final clamping guarantees the boundaries are strictly non-decreasing, with the endpoints fixed at 0 and 1.

The Statistics Accumulator

StatisticsAccumulator accumulates online within each iteration using three variables:

$$S_w = \sum_i w_i, \qquad S_{wf} = \sum_i w_i f_i, \qquad S_{wf^2} = \sum_i w_i f_i^2$$

At the end of an iteration, the estimates are

$$\mu = \frac{S_{wf}}{S_w}, \qquad \sigma^2 = \frac{S_{wf^2}}{S_w} - \mu^2$$

Note this is not the standard sample-variance formula — it is the weighted-variance formula, applicable when every sample has a different weight (the Jacobian weights of Vegas).

The One-Dimensional Convenience Function

integrate_1d(f, a, b, opts) internally performs the linear change of variables $x = a + u \cdot (b-a)$:

#![allow(unused)]
fn main() {
let width = b - a;
let wrapped = |u: &[f64]| f(a + u[0] * width) * width;
let mut vegas = Vegas::new(1, opts);
vegas.integrate(&wrapped)
}

The Jacobian factor $(b-a)$ is multiplied directly into the integrand; the user-supplied $f(x)$ receives physical coordinates rather than hypercube coordinates.


Advanced Topics

When to Use Vegas vs. Symbolic Integration

ScenarioRecommended method
The integrand has an elementary antiderivativeSymbolic integration (integrate) — exact result
The integrand involves special functions (Bessel, error functions, etc.)Numerical integration (Vegas)
High-dimensional integrals ($d > 3$)Monte Carlo (Vegas or others)
The integrand has singularities or boundary layersVegas + increase n_bins and iterations
Differentiable integral values are neededAutomatic differentiation (see Automatic Differentiation)

Limitations of Vegas

  1. Product-grid assumption: Vegas assumes the optimal sampling density factorizes as $\prod_k p_k(x_k)$. When the integrand has strong correlations between variables (such as $f(x,y) = \delta(x - y)$), the product grid cannot approximate it effectively and convergence slows down.

  2. Piecewise-constant approximation: the grid density is piecewise constant — sampling is uniform within each bin. If the integrand varies sharply inside a bin (e.g. a narrow peak), the variance estimate for that bin may be inaccurate.

  3. Adaptation bias: early iterations use a grid that has not yet converged, so their samples are not fully independent. Vegas mitigates this via inverse-variance weighting — converged iterations get larger weights.

  4. Deterministic seed: oCAS uses a fixed RNG seed (default 0x0C45) for reproducibility. This is useful in tests and benchmarks, but production environments should consider a random seed to avoid systematic bias.

Possible Improvements

  • Vegas+ (Lepage, 2020): introduces a "reflection" strategy into the grid update to handle symmetric integrands.
  • Suave (Hahn, 2005): combines the advantages of the Vegas adaptive grid with stratified sampling.
  • Divonne (Friedman & Harris, 1996): uses partitioning trees and heuristic subdivision strategies.
  • Cuhre: purely deterministic quadrature (not Monte Carlo), suitable for low-dimensional smooth integrands.

These methods could be added to oCAS by implementing the Integrator trait.


References

  1. Lepage, G. P. "A new algorithm for adaptive multidimensional integration." Journal of Computational Physics, 27(2):192–203, 1978. — The original paper on the Vegas algorithm.

  2. James, F. "Monte Carlo theory and practice." Reports on Progress in Physics, 43(9):1145–1189, 1980. — A survey of Monte Carlo methods, including a detailed description of Vegas.

  3. Lepage, G. P. "Vegas: An adaptive multi-dimensional integration program." Cornell preprint CLNS 80-447, 1980. — Implementation details and usage guide for Vegas.

  4. Hahn, T. "CUBA—a library for multidimensional numerical integration." Computer Physics Communications, 168(2):78–95, 2005. — A comparison of Vegas, Suave, Divonne, and Cuhre.

  5. Neal, R. M. "Annealed importance sampling." Statistics and Computing, 11(2):125–139, 2001. — The theoretical foundations of importance sampling.

  6. Kahn, H. & Marshall, A. W. "Methods of reducing sample size in Monte Carlo computations." Journal of the Operations Research Society of America, 1(5):263–278, 1953. — Early work on stratified sampling and importance sampling.

Gröbner Basis Implementation

This chapter details the implementation details of Gröbner basis computation in oCAS — algorithm selection, data structures, key optimizations, and the internal pipeline. For the mathematical theory see Gröbner Basis Theory; for API signatures see Rust API Reference.


Architecture Overview

flowchart TD
    A["groebner_basis(ideal, algo)"] --> B{algorithm selection}
    B -->|Auto| C["F4 (default)"]
    B -->|Buchberger| D["buchberger()"]
    B -->|F4| C
    B -->|F5| E["f5()"]

    C --> F{coefficient field?}
    F -->|FiniteField| G["f4_fp(): native i64 fast path over ℤ_p"]
    F -->|other fields| H["f4_generic(): generic path"]

    E --> I{coefficient field?}
    I -->|FiniteField| J["f5_fp(): native fast path over ℤ_p"]
    I -->|other fields| K["f5_generic(): generic path"]

    G --> L["output: GroebnerBasis"]
    H --> L
    J --> L
    K --> L
    D --> L

    L --> M{"order conversion needed?"}
    M -->|"zero-dimensional + different order"| N["fglm(): O(n·D³)"]
    M -->|"general ideal"| O["reorder(): re-run F4"]

The unified entry point groebner_basis accepts an Algorithm enum:

VariantBehavior
Autocurrently routes to F4 (the F5 switch point will be calibrated against cyclic-n benchmarks in the future)
Buchbergerclassical S-polynomial iteration
F4matrix batch reduction
F5signature criterion

All algorithms output a reduced Gröbner basis (GroebnerBasis::minimize().auto_reduce()).


The Buchberger Algorithm

GroebnerBasis::buchberger implements the classical Buchberger algorithm:

  1. Initialize the basis with the input polynomial set
  2. Construct all critical pairs (S-polynomials)
  3. For each pair, compute the S-polynomial and reduce it by multi-step division against the current basis
  4. If the remainder is nonzero, add it to the basis and update the critical pairs
  5. Repeat until no new polynomials are added
  6. Finally call minimize() (remove redundancy) and auto_reduce() (mutually reduce)
#![allow(unused)]
fn main() {
pub fn buchberger<D: Domain, O: MonomialOrder>(
    ideal: &[SparseMultivariatePolynomial<D, O>],
) -> GroebnerBasis<D, O> {
    GroebnerBasis::buchberger(ideal).minimize().auto_reduce()
}
}

Buchberger suits small ideals and teaching scenarios. Production should use F4.


The F4 Algorithm

F4 (Faugère 1999) replaces pairwise S-polynomial reduction with batch sparse-matrix row echelon computation. It is the default Gröbner basis algorithm in oCAS.

Main Loop

The structure of the f4 main loop:

initialize basis ← input polynomials
initialize pairs ← empty critical-pair set
for each initial polynomial: update_pairs(basis, pairs, simplifications, poly)

while pairs is not empty:
    (1) select all critical pairs from pairs with the minimum lcm degree
    (2) symbolic preprocessing: build the reduction matrix
    (3) row echelonization: Gaussian elimination
    (4) extract the surviving rows → new basis elements
    (5) for each new basis element: update_pairs(...)

Critical Pairs and the Gebauer–Moeller Criterion

CriticalPair stores the indices of two basis elements and the precomputed lcm:

#![allow(unused)]
fn main() {
struct CriticalPair {
    idx1: usize,       // index of the first basis element
    idx2: usize,       // index of the second basis element
    lcm: SmallVec<[usize; 4]>,  // lcm of LM(idx1) and LM(idx2)
    degree: usize,     // total degree of the lcm
}
}

update_pairs implements Gebauer–Moeller critical-pair management, following the description in Becker–Weispfenning's A Computational Approach to Commutative Algebra. Its core consists of three criteria:

First criterion (Chain Criterion): for an existing pair $(f_i, f_j)$, if there is an $f_k$ such that $\text{lcm}(f_i, f_k)$ divides $\text{lcm}(f_i, f_j)$ and $\text{lcm}(f_j, f_k)$ divides $\text{lcm}(f_i, f_j)$, then $(f_i, f_j)$ is redundant.

Second criterion (Update Criterion): when a new polynomial is added, scan the existing pairs; if the lcm of the new polynomial's LM with one element of a pair strictly divides the pair's lcm, that pair can be removed.

Redundant-pair cleanup: remove pairs whose leading terms are already reducible by other elements of the basis.

Symbolic Preprocessing

For the selected critical pairs, F4 builds a sparse matrix in which:

  • each row corresponds to a polynomial (an S-polynomial or a multiple of a basis element)
  • each column corresponds to a distinct monomial appearing in the matrix
  • monomials are sorted in descending order of the current term order (column 0 = largest monomial)

The worklist algorithm of the preprocessing:

  1. Add the two components of the S-polynomial pair to the matrix
  2. For each monomial $\mathbf{x}^\alpha$ in the matrix, if there is a basis element $f_i$ with $\text{LT}(f_i) \mid \mathbf{x}^\alpha$, add $f_i \cdot (\mathbf{x}^\alpha / \text{LT}(f_i))$ to the matrix
  3. Repeat until the worklist is empty

DivisorIndex: Fast Divisor Queries

A naive implementation needs an $O(\text{number of monomials} \times \text{basis size})$ linear scan to find reducers. oCAS replaces this with DivisorIndex:

#![allow(unused)]
fn main() {
struct DivisorIndex {
    buckets: HashMap<u64, Vec<usize>>,
}
}

Principle: the support of each monomial (the set of variables with positive exponent) is represented as a 64-bit mask. Basis elements are bucketed by the support mask of their leading monomial. On a query, the support of a reducer of $\mathbf{x}^\alpha$ must be a submask of $\text{support}(\alpha)$ — enumerate the submasks (bit operations) and do an exact divisibility check in the corresponding bucket.

When selecting a reducer, the basis element with the fewest terms is preferred (ties broken by basis index), matching Buchberger's linear-scan behavior.

SimpCache: Multiple Cache

#![allow(unused)]
fn main() {
type SimpCache<P> = Vec<(SmallVec<[usize; 4]>, P)>;
}

get_simplified looks up the already computed multiple basis_poly * x^diff for a given exponent difference diff in the cache. On a hit it returns directly, avoiding repeated multiplication. The cache persists across rounds, so the same S-polynomial multiple is constructed only once.

Row Echelonization

Once the matrix is built, sparse Gaussian elimination is performed:

ℤ_p fast path (echelonize_fp):

#![allow(unused)]
fn main() {
fn echelonize_fp(
    matrix: &mut Vec<Vec<(i64, usize)>>,
    ncols: usize,
    prime: i64,
    pivots: &mut Vec<Option<usize>>,
)
}
  • Sort the rows by leading column in ascending order
  • Scan column by column: find the first nonzero row in the current column as the pivot
  • Make the pivot row monic (multiply by the modular inverse of the leading coefficient)
  • Eliminate the current column of the remaining rows with the pivot row
  • sub_scaled_fp performs the sparse row subtraction: a two-pointer merge of two rows sorted by ascending column, skipping the leading column (the pivot is monic, so the leading column cancels automatically); the resulting coefficients are normalized to $[0, p)$, zero coefficients dropped

Generic path (echelonize_generic):

Same structure as the ℤ_p path, but uses the domain's generic sub/mul/div operations. sub_scaled_generic is likewise a two-pointer merge with zero coefficients dropped.

The Native ℤ_p Fast Path (FpPoly)

When the coefficient domain is FiniteField, F4 automatically switches to f4_fp, in which all polynomial operations use FpPoly — a polynomial representation with pure i64 modular arithmetic:

#![allow(unused)]
fn main() {
struct FpPoly {
    terms: Vec<(SmallVec<[usize; 4]>, i64)>,  // sorted descending by order; coefficients ∈ [0, p)
    n_vars: usize,
}
}

Constraint: $p < 2^{31}$, ensuring that the product of two residues still fits in i64.

register_row_fp implements a row cache across rounds, keyed by (basis_idx, diff), avoiding repeated construction of the same S-polynomial or reducer multiple.

monic_fp computes the modular inverse with the extended Euclid algorithm (mod_inv), normalizing the leading coefficient.

Performance impact: the ℤ_p path completely avoids BigInt allocations and uses lazy modular arithmetic in the row echelon step, bringing finite-field timings close to rational timings.

Basis Post-Processing

After the F4 loop:

  1. minimize(): remove redundant elements whose leading terms are divisible by the leading term of another basis element
  2. auto_reduce(): for each basis element, reduce its tail (the non-leading part) by the remaining basis elements

The F5 Algorithm

F5 (Faugère 2002) is a signature-based Gröbner basis algorithm. Its core idea: attach a "signature" to each polynomial and use the syzygy criterion to reject zero reducers before matrix construction, yielding order-of-magnitude speedups on hard ideals (such as cyclic-n).

Since version 0.19.0, F5 has been a production-grade implementation.

Signatures and the pot Order

#![allow(unused)]
fn main() {
struct Signature {
    module_pos: usize,                     // index of the input generator (0-based)
    monomial: SmallVec<[usize; 4]>,        // multiplier monomial
}
}

A signature records the "history" of a polynomial: module_pos is the index of the input generator it originates from, and monomial is the monomial multiple applied to the module basis vector $\mathbf{e}_{\text{module_pos}}$ of that generator.

Signatures are compared with the pot (position-over-term) order: first compare module_pos (smaller is more senior), then compare monomial with the monomial order $O$.

LabeledPoly: Polynomials with Signatures

#![allow(unused)]
fn main() {
struct LabeledPoly<D: Domain, O: MonomialOrder> {
    poly: SparseMultivariatePolynomial<D, O>,
    sig: Signature,
}
}

Every polynomial in the F5 basis carries a signature. LabeledPoly implements the BasisPoly trait, so it can reuse F4's critical-pair management (update_pairs) and simplification cache.

SyzygySet: Tracking Zero Reductions

#![allow(unused)]
fn main() {
struct SyzygySet {
    lms: HashMap<usize, Vec<SmallVec<[usize; 4]>>>,
}
}

When a matrix row reduces to zero, its signature is a syzygy. F5's syzygy criterion: if the signature of a future row is a monomial multiple of a known syzygy signature, that row will also reduce to zero and can be skipped immediately.

Internally, the leading monomials of the known syzygies are stored grouped by module position (module_pos). A query (k, t) checks whether any LM under position k divides t.

The F5 Main Loop

The structure of f5 resembles F4 with key differences:

for each input generator g_i (i = 0, 1, ...):
    attach the signature (i, 1) (unit monomial) to g_i
    add (i, 1) to the basis
    update_pairs(...)

while pairs is not empty:
    select a batch of critical pairs
    for each pair:
        construct the S-polynomial and compute its signature sig
        if syzygies.is_syzygy(sig):  ← key optimization: skip
            continue
    build_and_reduce(selected, basis, syzygies):
        (1) construct the matrix rows with signatures
        (2) row echelonization
        (3) rows that reduce to zero → add their signatures to syzygies
        (4) surviving rows → new basis elements (signatures retained)
    for each new basis element: update_pairs(...)

The ℤ_p Fast Path of F5

Like F4, F5 has an f5_fp variant using LabeledFpPoly (FpPoly + signature):

#![allow(unused)]
fn main() {
struct LabeledFpPoly {
    poly: FpPoly,
    sig: Signature,
}
}

All polynomial operations run in i64 modular arithmetic; BigInt conversions happen only at the input-reading and output-writing boundaries.


The FGLM Order-Conversion Algorithm

FGLM (Faugère–Gianni–Lazard–Mora 1993) converts the Gröbner basis of a zero-dimensional ideal from one monomial order to another at a cost of $O(n \cdot D^3)$ field operations ($D$ being the staircase dimension) — far cheaper than re-running F4 on a general ideal.

Core Idea

The staircase of a zero-dimensional ideal (the set of monomials not divisible by any leading monomial) is finite. FGLM traverses the monomials of the staircase in increasing target order, computes their normal forms with respect to the current basis, and detects linear dependencies:

compute_staircase(lms, n_vars):
    BFS from the unit monomial
    monomial m ∈ staircase ⟺ no LM divides m
    if the BFS does not terminate → positive dimension, return None

fglm(gb, TargetOrder):
    staircase ← compute_staircase(gb.lms)
    seen ← []  // normal forms of the traversed monomials (staircase coordinates)
    new_basis ← []

    traverse the monomials m of the staircase in increasing TargetOrder:
        nf ← normal_form_monomial(m, gb, staircase)
        coeffs ← find_relation(seen, nf)
        if coeffs exist:  // nf = Σ c_i · seen_i → linear dependency
            new_poly ← m - Σ c_i · m_i  // new basis element in target order
            new_basis.push(new_poly)
            mark_multiples(visited, m)  // skip multiples of m
        else:
            seen.push(nf)

    return GroebnerBasis::new(new_basis)

Normal Form Computation

normal_form_monomial reduces a monomial $m$ against the source basis GB and expresses the result as a linear combination of staircase coordinates — a vector of length $\dim(R/I)$, one component per basis monomial of the staircase.

Linear-Dependency Detection

find_relation uses Gaussian elimination over the field to detect whether coefficients $c_i$ exist such that $\text{nf} = \sum c_i \cdot \text{seen}_i$. If they exist, these coefficients determine the new basis polynomials.

When None Is Returned

When the ideal is positive-dimensional (the staircase is infinite), fglm returns None. In that case one should use reorder instead (re-run F4 in the new order).


Hilbert Series Computation

The hilbert module computes the Hilbert series and related invariants from a Gröbner basis.

Hilbert Numerator (Inclusion–Exclusion)

For a monomial ideal $\langle m_1, \dots, m_s \rangle$, the Hilbert numerator is computed by inclusion–exclusion:

$$N(t) = \sum_{k=0}^{s} (-1)^k \sum_{|S|=k} t^{\deg \text{lcm}(S)}$$

hilbert_numerator iterates over all subsets of the generators, computes the degree of the lcm, and accumulates with the inclusion–exclusion sign. It is practical for up to about 20 generators.

The Regularity Bound

regularity_bound returns the highest degree with a nonzero coefficient in the Hilbert numerator. This is an early-termination hint for F4 (Bayer–Stillman): F4 can stop selecting critical pairs whose degree exceeds the regularity bound — because all remaining S-polynomials will reduce to zero. Currently the bound is advisory; it does not change the computed result.

The Full Hilbert Series

hilbert_series uses Macaulay's theorem: the Hilbert series of $R/I$ equals the Hilbert series of $R/\text{LT}(I)$ (where $\text{LT}(I)$ is the leading-term ideal).

#![allow(unused)]
fn main() {
struct HilbertSeries {
    numerator: Vec<i64>,       // coefficients of N(t), from the constant term upward
    denominator_power: usize,  // power of the denominator (1-t)^n (= number of variables)
}
}

Four methods are provided:

  • hilbert_function(d): the value of $\dim_k (R/I)_d$ at degree $d$
  • dimension(): the Krull dimension of $R/I$
  • degree(): the degree of the projective variety
  • hilbert_polynomial(): Lagrange interpolation computes the full polynomial coefficients

The staircase dimension staircase_dimension is given by the value of the inclusion–exclusion numerator at $t = 1$ (the enumeration grows exponentially with the number of generators, so it is practical up to about 20); it returns None when the ideal is positive-dimensional (the numerator coefficients sum to 0).


Ideal Operations

The ocas_poly::ideal module implements complete ideal arithmetic on top of Gröbner bases. All operations use the Lex order for consistency.

Membership Testing

ideal_contains(gens, f, algo):

  1. Compute a Gröbner basis GB of the generators
  2. Reduce $f$ against GB
  3. The remainder is zero $\iff f \in I$

Sum and Product

Ideal sum $I + J = \langle f_1, \dots, f_m, g_1, \dots, g_n \rangle$: merge the two generator sets and compute the GB.

Ideal product $I \cdot J = \langle f_i \cdot g_j \rangle$: take all pairwise products and compute the GB.

Quotient (Rabinowitsch Trick)

$I : J = {f : f \cdot g \in I, \forall g \in J}$

For a single generator $g$, quotient_single_generator proceeds:

  1. Introduce a new variable $w$ in the extended ring $k[x_1, \dots, x_n, w]$
  2. Compute $\text{GB}(I' \cup {1 - wg'})$
  3. Eliminate $w$ (take the elements of the Lex GB not containing $w$)
  4. The result is $I : g \subset k[x_1, \dots, x_n]$

For multiple generators $J = \langle g_1, \dots, g_k \rangle$, compute $I : g_j$ separately and intersect.

Saturation

$I : J^\infty = \bigcup_k (I : J^k)$

ideal_saturate iterates $I : J$, $(I : J) : J$, …, until stabilization (two consecutive results equal).

Intersection (Auxiliary Variable Method)

$I \cap J = \langle t \cdot f_i, (1-t) \cdot g_j \rangle \cap k[x_1, \dots, x_n]$

intersect_generators:

  1. Introduce an auxiliary variable $t$ (index 0)
  2. Construct $t \cdot f_i$ and $(1-t) \cdot g_j$
  3. Compute the Lex GB
  4. Eliminate $t$ (take the elements not containing $t$)

Elimination

eliminate(gens, elim_vars, algo):

Exploits the natural elimination property of the Lex order: under Lex, a reduced GB automatically contains generators of the elimination ideal. Take the polynomials of the GB that do not involve the first elim_vars variables.


Zero-Dimensional Solving

solve_polynomial_system classifies the equation system and computes real solutions.

Dimension Detection

is_zero_dimensional checks that for each variable $x_i$ the GB contains a pure power $x_i^N$ as some leading monomial. Equivalently, the staircase (standard monomials) is finite.

Solving by Triangular Decomposition

For zero-dimensional systems, solve_triangular performs:

  1. Take the Lex GB (which naturally has triangular form)
  2. Back-substitute starting from the last variable (smallest in Lex):
    • extract the polynomial involving only the single variable $x_n$ → univariate equation → compute real roots
    • substitute each root of $x_n$ into the polynomials involving $x_{n-1}, x_n$
    • recursively solve for $x_{n-1}$, …
  3. Real roots are computed by Sturm root isolation (at f64 precision)

compute_vector_space_dim returns the vector-space dimension of $k[x_1, \dots, x_n]/I$ (the product of the degrees of the univariate polynomials in each variable).

Classification of Solutions

#![allow(unused)]
fn main() {
enum PolynomialSystemSolution {
    ZeroDimensional(ZeroDimSolutions),  // finitely many real solutions
    PositiveDimensional(GroebnerBasis), // infinite solution set; returns the Lex GB
    Empty,                               // no solution (ideal = ⟨1⟩)
}
}

Primary Decomposition and Radicals

Zero-Dimensional Radicals

radical_zero_dim: take the square-free decomposition of each univariate polynomial in the Lex GB, replace the original polynomials by their square-free factors, and recompute the GB.

Positive-Dimensional Radicals

radical_via_jacobian (a simplified Kemper algorithm, characteristic 0):

  1. Compute the partial derivatives $\partial f_i / \partial x_j$ of all generators with respect to all variables
  2. Take the GCD of all nonzero partial derivatives as $h$
  3. $\sqrt{I} = I : h^\infty$ (implemented via saturation)

If the Jacobian is trivial (all derivatives zero or $h = 1$), it falls back to returning the original GB.

Zero-Dimensional Primary Decomposition

primary_decomp_zero_dim:

  1. Take the irreducible factorization of the univariate polynomials in the Lex GB
  2. Each irreducible factor $p_i^{e_i}$ corresponds to one primary component
  3. Separate the components by saturation: $Q_i = I : \prod_{j \neq i} p_j^\infty$
  4. The associated prime ideal is $\mathfrak{p}_i = \langle p_i \rangle$

Returns Vec<PrimaryComponent>, each containing:

  • primary: the GB of the primary ideal
  • prime: the GB of the associated prime ideal

Primality and Primary Testing

  • is_prime_ideal: for zero-dimensional ideals, checks whether the univariate polynomial is irreducible; conservatively returns false for positive-dimensional ideals
  • is_primary_ideal: checks whether there is exactly one associated prime ideal

The Monomial Order System

oCAS supports a rich set of monomial order configurations (0.19.1+):

OrderDescription
Lexlexicographic: $x_1 > x_2 > \cdots$, suitable for elimination
Grlexgraded lexicographic: total degree first, then lexicographic
Grevlexgraded reverse lexicographic: total degree first, then reverse lexicographic
WeightOrderweighted order: descending by $\sum w_i e_i$
BlockOrderblock elimination: variables grouped into blocks, each with an independent sub-order
MatrixOrdergeneral weight-matrix order (0.23.0+)

MatrixOrder::elimination_order(elim_vars, n_vars) generates an elimination order: it eliminates the first elim_vars variables first, comparing the rest by Grevlex.

Order-Conversion Strategy

ScenarioRecommended strategy
zero-dimensional + Lex neededcompute with Grevlex first, then convert to Lex with fglm
general ideal + different order neededreorder (re-run F4 in the new order)
eliminationuse Lex directly or MatrixOrder::elimination_order

Benchmark Data

Criterion timings (cyclic systems, on this machine):

SystemBuchbergerF4Speedup
cyclic-3 ℚ308 µs147 µs2.1×
cyclic-4 ℚ3.99 ms2.13 ms1.9×
cyclic-3 ℤ₁₃582 µs276 µs2.1×
cyclic-4 ℤ₁₃6.19 ms2.80 ms2.2×

The native i64 ℤ_p fast path brings finite-field timings close to rational timings.


See Also

  • Mathematical theory: Gröbner Basis Theory — mathematical foundations of the Buchberger/F4/F5 algorithms, Hilbert-function theory
  • FGLM and elimination: FGLM and Elimination Theory — mathematical principles of order conversion, Gröbner-basis implementations of ideal operations
  • API reference: Rust Gröbner Bases and Ideals — function signatures, parameters, return values, complete examples
  • Polynomials: Polynomials — representation of SparseMultivariatePolynomial and the monomial-order traits
  • Coefficient domains: Coefficient Domains — the Domain trait and the domain implementations

Polynomial Factorization

oCAS implements polynomial factorization for both univariate and bivariate polynomials over the integers and over prime finite fields. This chapter describes the algorithms, their scope, and the public APIs used to access them.


Scope

Current factorization support covers:

DomainPolynomialsAlgorithms
$\mathbb{Z}[x]$UnivariateSquare-free factorization, Berlekamp–Zassenhaus, Hensel lifting
$\mathbb{F}_p[x]$UnivariateSquare-free factorization, Berlekamp
$\mathbb{Z}[x,y]$Bivariate (any LC)Wang's Hensel lifting; constant-LC fast path, non-constant LC via EEZ
$\mathbb{F}_p[x,y]$Bivariate (monic in $x$)Hensel lifting over $\mathbb{F}_p$
$\mathbb{Z}[x_1,\dots,x_n]$MultivariateWang EEZ Hensel lifting + leading-coefficient preprocessing + p-adic coefficient Hensel lift (non-constant LC) + skeleton-interpolation Diophantine + Zassenhaus recombination
$\mathbb{F}_p[x_1,\dots,x_n]$MultivariateEEZ Hensel lifting (with characteristic-$p$ $p$-th power handling; non-constant LC via field Wang preprocessing)
$\mathbb{Q}(\alpha)[x]$UnivariateTrager's algorithm: norm + factorization over $\mathbb{Q}$ + modular GCD over $\mathrm{GF}(p^d)$

Since 0.16.0, multivariate factorization with more than two variables is supported. Since 0.16.1, non-constant leading coefficients in the main variable are fully handled via a p-adic coefficient Hensel lift that imposes Wang's reconstructed leading coefficients at every iteration; the bivariate constant-LC fast path is retained for efficiency. Since 0.17.0, univariate factorization over algebraic number fields is supported (Trager).


Univariate Factorization over a Finite Field

For a prime field $\mathbb{F}_p$, oCAS uses Berlekamp's algorithm. The polynomial is first made square-free, then the Frobenius matrix $Q$ is built ($Q[i][j]$ = coefficient of $x^j$ in $x^{ip} \bmod f$) and the nullspace of $Q^T - I$ is computed. Every non-trivial nullspace vector $v$ satisfies $v^p \equiv v \pmod f$ and yields a splitting via $\gcd(f, v - a)$ ($a \in \mathbb{F}_p$). Small primes ($p \le 1000$) take the Berlekamp matrix path; larger primes use Cantor–Zassenhaus.

#![allow(unused)]
fn main() {
use ocas_domain::{FiniteField, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let p = FiniteField::new(Integer::from(101));
let mut f = DenseUnivariatePolynomial::<FiniteField>::from_coeffs(p.clone(), vec![
    p.element(1), // constant term
    p.element(0),
    p.element(1), // x^2
]);
let factors = f.factor();
}

The returned factorization is a list of (factor, multiplicity) pairs. Over finite fields, multiplicities are always 1 after the square-free step.


Univariate Factorization over the Integers

For $\mathbb{Z}[x]$, oCAS combines square-free factorization with Berlekamp–Zassenhaus-style Hensel lifting. The high-level steps are:

  1. Compute the content and reduce to a primitive polynomial.
  2. Compute the square-free decomposition.
  3. Choose a small prime $p$ such that the reduction stays square-free and has the same degree as the input.
  4. Factor modulo $p$ using Cantor–Zassenhaus (which internally uses the Berlekamp matrix method for small primes $p \le 1000$).
  5. Lift the modular factors to factors over $\mathbb{Z}[x]$ using Hensel lifting.

The actual call chain is: factor() → factor_primitive (Yun square-free decomposition; factors are emitted in ascending multiplicity order, so $k = 1$ factors come first) → for each square-free component factor_square_free (leading-coefficient transformation: for leading coefficient $a$ and degree $d$, build the monic polynomial $g(x) = a^{d-1} f(x/a)$; for $|a| = 1$ it goes straight to the monic path) → factor_square_free_monic (Mignotte coefficient bound $B$, modular factorization, Hensel lifting up to $p^k > 2B$, Zassenhaus subset recombination). Each monic factor $G$ of the transformed path is mapped back to $\mathbb{Z}[x]$ via the primitive part $\operatorname{pp}(G(a x))$.

#![allow(unused)]
fn main() {
use ocas_domain::IntegerDomain;
use ocas_poly::DenseUnivariatePolynomial;

let f = DenseUnivariatePolynomial::<IntegerDomain>::from_coeffs(
    IntegerDomain,
    vec![1.into(), 0.into(), 1.into()], // x^2 + 1
);
let factors = f.factor();
}

Bivariate Factorization over the Integers

ocas-poly provides bivariate factorization over $\mathbb{Z}[x,y]$ using Wang's Hensel lifting, assuming the polynomial is monic in $x$.

The algorithm:

  1. Choose an evaluation point $y = \alpha$ so that the univariate image $f(x, \alpha)$ is square-free and has the fewest irreducible factors.
  2. Factor the univariate image over $\mathbb{Z}[x]$.
  3. Lift the univariate factors back to bivariate factors by correcting the Taylor coefficients of $f$ around $y = \alpha$.

The correction step uses rational Bézout coefficients for the univariate factors, then reconstructs integral corrections with integer division. If the reconstruction fails (non-integral remainder) or the lifted product does not match the original polynomial, the implementation tries the next candidate evaluation point and eventually falls back to returning the polynomial as irreducible.

#![allow(unused)]
fn main() {
use ocas_domain::IntegerDomain;
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::sparse::Lex;

type MPoly = SparseMultivariatePolynomial<IntegerDomain, Lex>;

let domain = IntegerDomain;
let f = MPoly::from_terms(domain, 2, vec![
    (vec![3, 0], 1.into()),  // x^3
    (vec![2, 1], 1.into()),  // x^2*y
    (vec![2, 0], 2.into()),  // 2*x^2
    (vec![1, 1], 1.into()),  // x*y
    (vec![1, 0], 1.into()),  // x
    (vec![0, 2], 1.into()),  // y^2
    (vec![0, 1], 3.into()),  // 3*y
    (vec![0, 0], 2.into()),  // 2
]);

let factors = f.factor();
// factors contains (x^2 + y + 1, 1) and (x + y + 2, 1)
}

Bivariate Factorization over a Finite Field

Over $\mathbb{F}_p[x,y]$, the same Hensel-lifting structure is used, but the arithmetic is performed directly in the finite field. Bézout coefficients are computed with finite-field gcds, and all corrections are guaranteed to remain in the field, so no integral reconstruction is required.

#![allow(unused)]
fn main() {
use ocas_domain::FiniteField;
use ocas_poly::SparseMultivariatePolynomial;
use ocas_poly::sparse::Lex;

let domain = FiniteField::new(101.into());
type FpPoly = SparseMultivariatePolynomial<FiniteField, Lex>;

let f = FpPoly::from_terms(domain.clone(), 2, vec![
    (vec![2, 0], 1.into()), // x^2
    (vec![0, 1], 1.into()), // y
    (vec![0, 0], 1.into()), // 1
]);
let factors = f.factor();
}

C/C++ Polynomial API

The C bindings expose opaque handles for bivariate integer and finite-field polynomials. Polynomials are created from ASCII strings, factored, printed, and freed through the C API.

#include <ocas.h>
#include <stdio.h>

int main(void) {
    int err = 0;
    OcasPolyZ *f = ocas_poly_z_create("x^2 + y + 1", &err);
    if (f == NULL) {
        fprintf(stderr, "parse error: %s\n", ocas_error_last_message());
        return 1;
    }

    OcasPolyFactorArray factors = {0};
    int rc = ocas_poly_z_factor(f, &factors, &err);
    if (rc != OCAS_OK) {
        fprintf(stderr, "factor error: %s\n", ocas_error_last_message());
        ocas_poly_z_free(f);
        return 1;
    }

    printf("factors: %zu\n", factors.len);
    for (size_t i = 0; i < factors.len; ++i) {
        OcasPolyZ *factor = (OcasPolyZ *)factors.factors[i].poly;
        char *s = ocas_poly_z_to_string(factor, &err);
        printf("  %s^%zu\n", s, factors.factors[i].multiplicity);
        ocas_string_free(s);
        ocas_poly_z_free(factor);
    }
    ocas_poly_factor_array_free(&factors);
    ocas_poly_z_free(f);
    return 0;
}

For finite fields, use OcasPolyFp, ocas_poly_fp_create, and ocas_poly_fp_factor. The factor array stores void* polynomial handles, so the caller must cast the pointer back to the correct concrete type before printing or freeing it.


Multivariate Factorization (Wang EEZ)

Since 0.16.0, oCAS factors polynomials in any number of variables over $\mathbb{Z}$ and $\mathbb{F}_p$ using Wang's EEZ (Evaluation and EZ-lifting) algorithm:

  1. Square-free factorization (Yun): differentiate in the main variable $x_1$ and peel repeated factors via $n$-variate GCD (dense recursive evaluation–interpolation); characteristic-$p$ $p$-th powers are shrunk and expanded.
  2. Sample-point search: substitute the secondary variables $x_2,\dots,x_n$ at sample points so the univariate image keeps its degree and stays square-free.
  3. Wang leading-coefficient preprocessing: factor the leading coefficient $\ell(x_2,\dots)$ and distribute its factors among the univariate factors using their pairwise-coprime integer images, reconstructing the true leading coefficients $\ell_i$.
  4. Variable-by-variable Hensel lifting: lift the univariate factors back to multivariate ones through the ideals $(x_k - a_k)$, solving multivariate Diophantine equations at each step.
  5. Zassenhaus recombination: when the lifted factors are finer than the true factors, enumerate subsets and trial-divide to obtain the irreducible factors.
#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::sparse::{Lex, SparseMultivariatePolynomial};

// (x + y + z)(x - y + 2z)
let f = SparseMultivariatePolynomial::<IntegerDomain, Lex>::from_terms(
    IntegerDomain, 3,
    vec![
        (vec![2, 0, 0], Integer::from(1)),
        (vec![1, 0, 1], Integer::from(3)),
        (vec![0, 1, 1], Integer::from(1)),
        (vec![0, 2, 0], Integer::from(-1)),
        (vec![0, 0, 2], Integer::from(2)),
    ],
);
let factors = f.factor();
}

The result is a list of (factor, multiplicity) pairs, normalized to be primitive with a positive leading coefficient.


Factorization over Algebraic Number Fields (Trager)

Since 0.17.0, oCAS factors univariate polynomials over an algebraic number field $K = \mathbb{Q}(\alpha)$, where $\alpha$ is given by a monic minimal polynomial over $\mathbb{Q}$. The domain lives in ocas-domain as AlgebraicExtension<D> (AlgebraicNumberField for $D = \mathbb{Q}$); the same type with $D = \mathbb{F}_p$ represents $\mathrm{GF}(p^d)$.

The factorizer implements Trager's algorithm:

  1. Square-free factorization (Yun) over $K$, using the modular GCD below instead of a pseudo-remainder sequence.
  2. Norm with shift: for $s = 0, 1, 2, \dots$ compute the norm of $g(x) = f(x - s\alpha)$ down to $\mathbb{Q}[x]$ by evaluation–interpolation of the scalar resultant $\operatorname{Res}_\alpha(m, g)$, until the norm is square-free (checked modulo small primes; acceptance is exact).
  3. Rational factorization: the square-free norm is factored over $\mathbb{Z}$ (Hensel path).
  4. Modular GCD over $K$: each rational factor is mapped into $K[x]$ and its GCD with $g$ is computed by the modular method — map to $\mathrm{GF}(p^d)$ for primes with $m$ irreducible mod $p$, combine monic modular GCDs by CRT, rational-reconstruct the coefficients, and verify by trial division.
  5. Shift back with $x \mapsto x + s\alpha$ and normalize monic.

A rational fast path applies when all coefficients of $f$ are rational constants: $f$ is first factored over $\mathbb{Q}$, and only the $\mathbb{Q}$-irreducible factors go through Trager's norm machinery.

#![allow(unused)]
fn main() {
use ocas_domain::{AlgebraicNumberField, Domain, Rational, RationalDomain};
use ocas_poly::DenseUnivariatePolynomial;

// ℚ(√2): minimal polynomial α² − 2.
let field = AlgebraicNumberField::new(
    RationalDomain,
    vec![Rational::new(-2, 1), Rational::new(0, 1), Rational::new(1, 1)],
);
// x² − 2 = (x − √2)(x + √2) splits over ℚ(√2).
let f = DenseUnivariatePolynomial::from_coeffs(
    field.clone(),
    vec![field.from_base(Rational::new(-2, 1)), field.zero(), field.one()],
);
let factors = f.factor();
assert_eq!(factors.len(), 2);
}

Benchmarks (criterion group poly_factor_anf): degree ≤ 12 over $\mathbb{Q}(\sqrt2)$, $\mathbb{Q}(\sqrt[3]{2})$, and the cyclotomic field $\mathbb{Q}(\zeta_5)$ all factor in well under 100 ms.


Limitations and Future Work

  • Non-constant leading coefficients in the main variable are fully supported since 0.16.1 via a p-adic coefficient Hensel lift with skeleton-interpolation Diophantine.
  • The bivariate Wang Hensel limitation that the leading coefficient be a constant is subsumed by the 0.16.0 arbitrary-multivariate (EEZ) path; bivariate non-constant-LC inputs now dispatch to the EEZ path automatically.
  • Since 0.16.2 the $\mathbb{F}_p$ multivariate path also supports non-constant leading coefficients via field Wang LC preprocessing (wang_reconstruct_lcoeffs_fp) and eez_lift_imposed.
  • Algebraic number field factorization (0.17.0) is currently univariate only; extension-field factorization of multivariate polynomials and a RootOf/radical syntax in the parser are future work. The minimal polynomial's irreducibility is the caller's responsibility (over a reducible modulus the ring has zero divisors and inversion fails).
  • The adaptive evaluation-point search has been widened (7 → 25 for $\mathbb{Z}$, 8 → 32 for $\mathbb{F}_p$); very sparse or highly specialized polynomials may still need an extended range or additional Diophantine interpolation passes.

These limitations are tracked in the project roadmap and will be lifted as the algebra kernel matures.

Number Theory

The ocas_domain::number_theory module provides the computational number-theory stack: multi-modulus Chinese remaindering, BPSW primality, integer factorization (trial division, Pollard rho/p−1, Williams p+1, ECM), discrete logarithms, and the classical multiplicative functions. Python and C bindings expose the same functionality.

Primality Testing

  • is_prime(n): deterministic for $n < 3.3\cdot10^{24}$.
  • primes::is_prime_bpsw(n): base-2 strong Miller–Rabin + strong Lucas test (Selfridge parameters); no composite is known to pass it.
  • primes::is_prime_u64(n): deterministic over the whole u64 range.

Integer Factorization

#![allow(unused)]
fn main() {
use ocas_domain::Integer;
use ocas_domain::number_theory::factor::factor_integer;

let f = factor_integer(&Integer::from(360));
// [(2, 3), (3, 2), (5, 1)]
}

factor_integer peels small factors by trial division (primes up to 1000), then splits each composite cofactor with an escalating strategy: one quick Pollard-rho attempt, then rounds of Pollard p−1 / Williams p+1 / ECM (Suyama parametrization, Montgomery curves, stage 1) with growing smoothness bounds (the curve budget scales as ≈ B1/550). A 30-digit semiprime factors in about a second in release mode.

Chinese Remainder Theorem

crt::crt_many(&[(r1, m1), (r2, m2), ...]) merges any list of congruences into a single congruence x ≡ R (mod M); the moduli need not be pairwise coprime, and inconsistent systems return None.

Discrete Logarithms

  • dlog::dlog_bsgs(base, target, modulus): baby-step giant-step, practical for small groups.
  • dlog::dlog_pohlig_hellman(base, target, p): factors the order of base modulo the prime p and combines the subgroup logarithms via CRT.

Number-Theoretic Functions

functions::euler_phi, moebius_mu, divisor_tau, divisor_sigma(n, k), liouville_lambda — all computed from the prime factorization of |n|. Quadratic-residue tools (legendre, jacobi, Tonelli–Shanks mod_sqrt) live in the parent module.

Python Bindings

Note the argument order of two functions: discrete_log(p, base, target) takes the modulus first (it solves base^x ≡ target (mod p) for x); crt(moduli, residues) takes the modulus list first and the residue list second.

import ocas

ocas.factorint(360)            # [("2", 3), ("3", 2), ("5", 1)]
ocas.isprime(2**61 - 1)        # True
ocas.nextprime(10**6)          # 1000003
ocas.discrete_log(101, 2, 66)  # 83
ocas.crt([3, 5, 7], [2, 3, 2]) # (23, 105)
ocas.jacobi_symbol(2, 7)       # 1
ocas.totient(36)               # 12
ocas.mobius(30)                # -1
ocas.divisor_count(12)         # 6
ocas.divisor_sigma(12, 2)      # 210
ocas.liouville_lambda(12)      # -1

C Bindings

The C API (ocas_ntheory_*, declared in include/ocas.h) passes integers as decimal strings; results are heap strings released with ocas_string_free. RAII wrappers live in ocas::ntheory (include/ocas.hpp).

Modular Polynomial GCD

ocas_poly::gcd::modular::gcd_modular_z computes the primitive GCD of two dense univariate integer polynomials with Brown's modular algorithm: monic GCD images modulo several primes are combined with CRT (symmetric representatives) and confirmed by exact trial division. It replaces the naive pseudo-remainder GCD, whose coefficients explode for degrees ≳ 16, and handles degree-50 inputs with 100-digit coefficients. The bivariate gcd_modular applies the same strategy with content separation, monic interpolation images, and rational reconstruction.

Symbolic Integration

oCAS integrates expressions by a layered pipeline: a fast heuristic table, then a rational-function integrator, then the Risch algorithm over elementary towers, then a trigonometric-to-exponential rewrite, and finally a special-function table. The first layer that produces an answer wins. This chapter explains each layer and when the unevaluated Integral(expr, var) form is returned.


The Pipeline

integrate(expr, var) tries, in order:

  1. Fast table — inline structural rules: power rules (x^n, (ax+b)^n, including fractional exponents), sin/cos/exp of linear arguments, and direct table entries such as log(x). Fast and always attempted first.
  2. Rational-function integrator — Hermite reduction plus the logarithmic part (logarithmic-derivative identity, completing the square, Rothstein–Trager). Handles every rational function of x.
  3. Symbolic-constant rational integrator (0.27) — rational functions with coefficients in ℚ(symbols): Yun squarefree factorization + Hermite + partial fractions (linear residues, quadratic factors via log/atan/atanh).
  4. Risch algorithm — elementary transcendental towers built from log and exp (the tower recursion is capped by MAX_RISCH_DEPTH = 16).
  5. Trigonometric rewrite — sin/cos/tan/… rewritten into exp(I·x) and re-integrated by Risch, then converted back to real form on a best-effort basis.
  6. Special-function table — non-elementary integrals with closed forms in terms of erf, Ei, Si, Ci, Fresnel S/C, …
  7. Rule-table engine (0.27) — a self-contained standard-calculus rule library (families A–H: powers/binomials, exponentials/logarithms, trigonometry, hyperbolic, inverse trig/hyperbolic, rational intercepts, radicals, special forms); templates may carry residual Integral(g, x) reductions. Disable with IntegrateOptions { rules: false }.
  8. Trig product-to-sum / power reduction (0.27.x) — products and powers of sin/cos at linear arguments reduce to multiple-angle sums, integrated termwise.
  9. Heuristic techniques — integration by parts, trigonometric substitution, Weierstrass $t = \tan(x/2)$, Euler substitutions.
  10. Bounded expansion retry (0.27.x) — products are distributed over sums (64-term budget) and integrated termwise.
  11. Unevaluated form — Integral(expr, var).

Rational Functions

Every rational function of the integration variable is integrated exactly. The polynomial part is integrated termwise; the proper fraction is split by Hermite reduction into a rational part plus a remainder with a squarefree denominator; the remainder yields logarithms (via the identity c·f'/f → c·log(f)), arctangents (degree-2 denominators by completing the square), or Rothstein–Trager logarithms.

#![allow(unused)]
fn main() {
use ocas::prelude::*;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse(&ctx, "(2*x + 3)/(x^2 + 3*x + 5)").unwrap();
let result = integrate(&ctx, expr, Symbol::new("x"));
// log(x^2 + 3*x + 5)
}

The Risch Algorithm

Elementary transcendental integrands are handled by building a differential field tower ℚ(x, t₁, …, tₙ) where each tᵢ is a log or exp over the field below, and integrating recursively (Bronstein, Symbolic Integration I, ch. 5):

  • at each level, the rational part is split off by Hermite reduction (at hyperexponential levels the t-power denominator is split off first as a Laurent polynomial, 0.29.0);
  • the logarithmic part first tries the logarithmic-derivative identity, then the Rothstein–Trager resultant method (rational roots only, 0.29.0);
  • the polynomial part is integrated by undetermined coefficients at log levels and by the Risch differential equation Dq + f·q = g at exp levels — solved over the full rational fragment since 0.29.0 (weak normalization, denominator bounds, degree bounds, SPDE, cancellation recursions);
  • the base ℚ(x) delegates to the rational-function integrator.

The tower recursion is capped by MAX_RISCH_DEPTH = 16: beyond that depth the Risch layer gives up and hands the integrand to the next layer (this keeps pathological integrands from recursing forever).

#![allow(unused)]
fn main() {
use ocas::prelude::*;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
// ∫ x·exp(x) dx = (x - 1)·exp(x)
let result = integrate(&ctx, parse(&ctx, "x*exp(x)").unwrap(), Symbol::new("x"));
}

Scope limits

Since 0.29.0 the Risch differential equation is solved over the full rational fragment (denominator bounds included), and the logarithmic part uses Rothstein–Trager with rational roots only — irrational or non-constant resultant roots, and the unported limited_integrate / higher-level parametric_log_deriv refinements, decline honestly. Consequences:

  • ∫ exp(x)/x dx has no elementary antiderivative — it is answered by the special-function table as Ei(x).
  • Some log-tower cases needing a free-constant choice that makes lower layers integrable (e.g. log(x+1)) are not yet decided and fall back.

When no layer succeeds, the result is the unevaluated form Integral(expr, var) — a deliberate answer, not an error.


Trigonometric Integrands

sin, cos, tan, cot, sec, csc are rewritten into complex exponentials via t = exp(I·x) and integrated by Risch. The imaginary unit is carried as a constant tower generator (D I = 0). Results are converted back to real form where possible: conjugate logarithm pairs merge into real log/atan terms.

The Risch differential-equation solver currently works over ℚ[x], so hyperexponential equations whose coefficients contain I (e.g. the ones produced by sin(x)·cos(x) or cos(x)²) cannot be solved yet; those integrands return the unevaluated form. Simple sin/cos of linear arguments are covered by the heuristic table.


Special Functions

Integrals with no elementary antiderivative but a standard closed form are answered directly (definitions match SymPy):

IntegrandResult
exp(-x²)(√π/2)·erf(x)
exp(x²)(√π/2)·erfi(x)
exp(c·x²), c < 0√π/(2√(-c))·erf(√(-c)·x)
exp(x)/xEi(x)
sin(x)/xSi(x)
cos(x)/xCi(x)
sinh(x)/xShi(x)
cosh(x)/xChi(x)
sin(x²)√(π/2)·fresnels(√(2/π)·x)
cos(x²)√(π/2)·fresnelc(√(2/π)·x)
#![allow(unused)]
fn main() {
use ocas::prelude::*;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
// ∫ exp(-x^2) dx = (√π/2)·erf(x)
let result = integrate(&ctx, parse(&ctx, "exp(-x^2)").unwrap(), Symbol::new("x"));
}

Fuel-bounded integration

integrate_with_fuel wraps the same pipeline but threads a [Fuel] budget through the two post-integration simplification stages. A pathological result that would otherwise spin the rewriter can be cut off deterministically.

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;
use ocas_core::fuel::Fuel;
use ocas_atom::{AtomArena, Symbol};
use ocas_calc::integral::integrate_with_fuel;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ctx.var("x");
let fuel = Fuel::new(500);
let result = integrate_with_fuel(&ctx, expr, Symbol::new("x"), &fuel);
match result {
    Ok(r) => println!("{}", r),
    Err(_) => println!("fuel exhausted during simplification"),
}
}

Returns Err only when fuel was exhausted mid-simplification. The integration traversal itself uses the internal depth limit; fuel bounds only the post-simplification passes.


Bindings

The same pipeline backs the Python and C APIs:

  • Python: Expression.integrate(var)
  • C: ocas_expr_integrate(...)

Both return the unevaluated Integral(...) form when no closed form is found, exactly like the Rust API. Neither certifies its result; use Expression.integrate_outcome (Python) or ocas_expr_integrate_outcome (C) when a machine-checkable certificate is required.


0.28.0: certificates, cycle detection and residue resolution

Symbolic certificates and the three-valued outcome

integrate_outcome (exposed to Rust, Python and C) returns one of three values:

ValueMeaning
Found { value, certificate }An antiderivative with a machine-checkable certificate (D(F) − f ≡ 0 in the exact checker)
ProvedNonElementary { witness }Proved non-elementary (no producer in 0.28.0; reserved for the non-elementary layer)
Unknown { residue, uncertified }Honest "I do not know": residue is the unevaluated Integral(f, x); uncertified is the pipeline's candidate (not an answer, diagnostics only)

The certificate engine (ocas-calc/src/integral/certify.rs) tries three layers: structural zero (normalize plus like-term collection), elementary-field zero (trig→exp rewriting, I² = −1 reduction, dependent exp/log atoms merged before embedding into a rational function field), and a reserved radical layer. Every layer is sound: treating atoms as independent generators can only miss an identity (a false negative), never certify a wrong answer. The field arithmetic runs under deterministic budgets and declines honestly when they are exhausted.

ocas-calc/src/integral/risch.rs additionally verifies every Risch result inside the tower's own field, so the engine cannot emit an answer the exact checker falsifies — that guard is how 0.28.0 caught and contained a latent wrong answer (the exp-level rational part mis-scaled negative powers).

Expression-level cycle detection and residue resolution

A chain entry (integral/chain.rs) is identified by the expression address plus rule_depth and parts_depth: a repeat of the same shape with a smaller budget is the pipeline's legitimate recursive descent and must be allowed; only an equal-budget repeat is a true cycle. The absolute entry backstop stays at 0.27.3's 256, and residue-resolution re-entries charge a separate budget so they cannot starve the primary chain.

Several stages return a partial result (the partial fraction is done and the leftover Integral(...) is itself solvable). After the chain is finished the top-level entry resolves those residues: only "rational shape, ≤2 symbols, ≤64 nodes" residues are retried, and a resolution is accepted only when the residue count strictly decreases. A plain Integral(f, x) fallback is never resolved (that would re-run the whole chain, and with rules = false would defeat the caller's intent).

Dependent-generator merging (regular towers)

ocas-calc/src/tower/merge.rs merges algebraically dependent generators through exact field identities: exp(u)/exp(u+c), exp(u)/exp(−u), log(u)/log(cu), log(u)/log(u^k), log(exp(u)) and exp(log(u)), registering constant generators (log(2), exp(1), D t = 0) when needed. The tower now carries the rewritten integrand, which is what risch converts; undecidable dependencies are still declined honestly.

Rational Functions, Resultants & Partial Fractions

oCAS provides a complete stack for rational function arithmetic, resultant computation, and partial fraction decomposition over any Euclidean domain. These capabilities were added in version 0.12.0, closing the gap with Symbolica's rational_polynomial.rs, resultant.rs, and partial_fraction.rs.


Rational Polynomials

A RationalPolynomial<D, O> represents an element of the fraction field of a polynomial ring — that is, a ratio $\frac{p}{q}$ where $p$ and $q$ are multivariate polynomials over a domain D.

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::{RationalPolynomial, SparseMultivariatePolynomial, Lex};

// Create polynomials: x + 1 and x - 1
let x_plus_1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
    IntegerDomain, 1,
    vec![(vec![0], Integer::from(1)), (vec![1], Integer::from(1))],
);
let x_minus_1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
    IntegerDomain, 1,
    vec![(vec![0], Integer::from(-1)), (vec![1], Integer::from(1))],
);

// (x+1) / (x-1)
let rat = RationalPolynomial::from_num_den(x_plus_1, x_minus_1);
}

Canonicalization

When constructed via from_num_den, the fraction is automatically reduced to canonical form:

  1. The GCD of numerator and denominator is divided out.
  2. The leading coefficient of the denominator is normalized (positive for ordered domains, 1 for finite fields).

Arithmetic

All standard operations are supported:

OperationMethodStrategy
Additiona.add(&b)Cross-multiply, then canonicalize
Subtractiona.sub(&b)Via negation + addition
Multiplicationa.mul(&b)Cross-cancel GCDs, then multiply
Divisiona.div(&b)Via b.inv() + multiplication; returns None when the numerator of b is zero
Negationa.neg()Negate numerator
Inversea.inv()Swap numerator/denominator; returns None when the numerator is zero
Powera.pow(n)Repeated squaring

Resultants

The resultant of two polynomials $a$ and $b$ is a scalar that is zero if and only if $a$ and $b$ share a common root (or equivalently, a non-trivial GCD).

#![allow(unused)]
fn main() {
use ocas_domain::{IntegerDomain, Integer};
use ocas_poly::DenseUnivariatePolynomial;

let d = IntegerDomain;
// Res(x^2 + 1, (x+1)^2) = 4
let a = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(1), Integer::from(0), Integer::from(1),
]);
let b = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Integer::from(1), Integer::from(2), Integer::from(1),
]);
assert_eq!(a.resultant(&b), Integer::from(4));
}

Algorithm: Brown's PRS

oCAS uses Brown's Polynomial Remainder Sequence algorithm, which avoids constructing the full Sylvester matrix. The algorithm tracks leading coefficients and degree differences to compute the resultant from the PRS using the fundamental theorem formula.

For two polynomials of degree 15, the resultant completes in under 20 ms.


Partial Fractions

Given a proper fraction $\frac{p(x)}{q(x)}$ (where $\deg(p) < \deg(q)$), the partial fraction decomposition expresses it as a sum of simpler fractions.

#![allow(unused)]
fn main() {
use ocas_domain::{RationalDomain, Rational};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_calc::partial_fraction::apart;

let d = RationalDomain;
let num = DenseUnivariatePolynomial::from_coeffs(d, vec![Rational::new(1, 1)]);
let den = DenseUnivariatePolynomial::from_coeffs(d, vec![
    Rational::new(1, 1), Rational::new(0, 1), Rational::new(-1, 1),
]);
let (poly_part, terms) = apart(&num, &den);
// poly_part is None (proper fraction)
// x^2 - 1 is square-free → exactly one term (1/(x^2 - 1), exp = 1)
}

Algorithm

The decomposition proceeds in steps:

  1. Polynomial division: If $\deg(p) \geq \deg(q)$, extract the polynomial part via div_rem.
  2. Square-free factorization: Decompose the denominator into square-free factors $f_i^{e_i}$.
  3. Diophantine CRT: For multiple factors, solve the polynomial Chinese Remainder Theorem to split the fraction.
  4. p-adic expansion: For repeated factors ($e_i > 1$), expand the numerator with respect to each factor to get individual terms.

The together(poly_part, &terms) function reverses the decomposition, returning the combined (numerator, denominator) pair.


Helper Methods

Several new methods were added to DenseUnivariatePolynomial to support these algorithms:

MethodDescription
extended_gcd_poly(&self, other)Extended GCD: returns (g, s, t) with $s \cdot a + t \cdot b = g$
diophantine(polys, b)Polynomial CRT solver
p_adic_expansion(&self, p)Repeated division expansion
pow(n)Polynomial power by repeated squaring
lcoeff()Leading coefficient (convenience)
constant()Constant term (convenience)
mul_coeff(c) / div_coeff(c)Scalar multiply/divide all coefficients
content()GCD of all coefficients (now public)

For SparseMultivariatePolynomial:

MethodDescription
div_exact(&self, divisor)Exact polynomial division (no remainder)
degree_in(var_index)Degree in a specific variable

Rational Reconstruction

The rational_reconstruction function recovers a rational number $\frac{n}{d}$ from its modular image $a \equiv \frac{n}{d} \pmod{m}$:

#![allow(unused)]
fn main() {
use ocas_domain::{Integer, IntegerDomain};
use ocas_poly::rational_reconstruction::rational_reconstruction;

// 3/7 mod 101: 7^{-1} mod 101 = 29, so a = 3 * 29 mod 101 = 87
let m = Integer::from(101);
let a = Integer::from(87);
let result = rational_reconstruction(&a, &m);
assert!(result.is_some());
}

This uses the extended Euclidean algorithm (continued fraction approach) and is useful for modular algorithms that need to lift results back to $\mathbb{Q}$.


Karatsuba Multiplication

Dense polynomial multiplication now uses Karatsuba's algorithm for polynomials with 32 or more coefficients. This reduces the multiplication complexity from $O(n^2)$ to $O(n^{1.585})$.

The threshold (32) was chosen empirically — below this size, the overhead of Karatsuba's extra additions and subtractions outweighs the savings from fewer multiplications.

For two degree-500 polynomials over $\mathbb{Z}$, Karatsuba provides a significant speedup over the previous schoolbook implementation.


SymPy Migration

SymPyoCAS
sp.apart(expr, x)apart(&num, &den)
sp.together(expr)together(poly_part, &terms)
sp.resultant(a, b, x)a.resultant(&b)
sp.Rational(n, d)Rational::new(n, d)

Limitations

  • Partial fraction decomposition currently works over any EuclideanDomain using square-free factorization. Full factorization into irreducibles requires IntegerDomain or FiniteField.
  • Multivariate partial fractions (apart_multivariate) are deferred to a future version.
  • FFT/NTT multiplication is available since 0.12.1 via the ntt feature.

ODE Solving Algorithms

oCAS's ODE module (ocas-calc/src/ode/) implements a symbolic solver for ordinary differential equations. This chapter details the classification criteria for each ODE type, the implementation details of the solving algorithms, and the design choices. For the mathematical derivations, see ODE Solving Theory.


Overall Architecture

The core entry point of ODE solving is dsolve, which performs three steps:

  1. Normalization (normalize_ode): algebraically simplifies and standardizes the input equation
  2. Classification (classify_ode): returns all applicable solving methods in priority order
  3. Dispatch: tries the methods in turn and returns the first successful result
input equation ─→ normalize ─→ classify ─→ dispatch ─→ ODESolution
                              │
          ┌───────────────────┼───────────────────┐
          │                   │                   │
     first-order ODE    second-order linear ODE   series solutions (fallback)
    ┌────┴────┐          ┌────┴────┐
 separable  linear   const-coeff  Cauchy-Euler
 Bernoulli  exact    reduction    variation of
 homogeneous          of order    parameters

Result Types

The ODESolution enum represents the outcome:

VariantMeaningTypical source
Explicit(expr)explicit solution $y = f(x)$linear, constant coefficient, Laplace
Implicit(expr)implicit solution $F(x, y) = C$separable, exact, homogeneous
Parametric(x(t), y(t))parametric solution $(x(t), y(t))$reserved in the enum; no solver produces it yet
Series(expr, n)truncated series solutionpower series, Frobenius
System(&[Atom])system solution components $(y_1, y_2)$$2\times2$ systems
Unsolved(ode)could not be solved symbolicallywhen all methods fail

The Classification Engine

The classifier (classify.rs) performs a structural analysis of the ODE and returns the list of candidate methods in priority order. It does not solve — only pattern recognition.

Detection Pipeline

First-order ODEs (order == 1):

  1. Check is_first_order_linear: $y$ and $y'$ appear only to the first power → LinearFirst
  2. Check is_bernoulli: some $y^n$ ($n \geq 2$) plus a linear $y$ term → Bernoulli
  3. Check is_separable: additive terms without $y$ plus additive terms with $y$ → Separable
  4. Check is_exact: in $M + Ny' = 0$, $\partial M/\partial y = \partial N/\partial x$ → Exact
  5. Check is_homogeneous: all terms have the same total $(x, y)$ degree → Homogeneous

Second-order and higher linear ODEs:

  1. is_constant_coeff_linear: the coefficients of $y, y', y''$ do not contain $x$ → LinearConstantCoeff
  2. is_cauchy_euler: the terms are of the form $c_k x^k y^{(k)}$ → CauchyEuler
  3. Second-order linear → ReductionOfOrder (tries simple candidate solutions)

Global fallback:

  1. Linear ODEs → PowerSeries (power series or Frobenius)

Exactness Detection

The exact-equation test is_exact works as follows:

  1. Split the equation into $M(x, y) + N(x, y),y' = 0$ with split_mn
  2. Replace $y(x)$ by the bare symbol $y$ so that $M, N$ become bivariate expressions
  3. Compute $\partial M/\partial y$ and $\partial N/\partial x$
  4. First compare the normalized strings (to_string() after normalize)
  5. If they differ, compute the difference and check with the rule-based simplifier whether it is zero
#![allow(unused)]
fn main() {
let dm_norm = normalize(ctx, dm_dy);
let dn_norm = normalize(ctx, dn_dx);
if dm_norm.to_string() == dn_norm.to_string() {
    return true;
}
// fallback: simplify the difference
let difference = simplify(ctx, ctx.add(&[dm_dy, ctx.mul(&[ctx.num(-1), dn_dx])]), &rules, 20);
matches!(normalize(ctx, difference).node(), AtomNode::Num(0))
}

This double-check strategy compensates for the simplifier's inability to collect like terms.

Homogeneity Detection

is_homogeneous checks whether the equation is homogeneous in $(x, y)$ (zero-th degree homogeneity). In the implementation, all_terms_homogeneous_degree walks all additive terms, computes the total degree of each term in $x$ and func (term_degree), and asserts that all terms have the same, positive degree.

For $\text{Derivative}(y(x), x)$, the degree is counted as 1 in $y$ (without additional powers of $x$).


First-Order ODE Solvers

The first-order solvers are implemented in first_order.rs; there are five methods.

Separable Equations

Criterion: the equation can be written as $g(y),y' = f(x)$.

Algorithm:

  1. Split the equation into $y$-terms and non-$y$ ($x$) terms with separate_by_func
  2. Replace $x$ by $y$ in the $y$-terms (substitute_var)
  3. Integrate both sides: $\int g(y),dy$ and $\int f(x),dx$
  4. Return the implicit solution $\int g(y),dy - \int f(x),dx = C$

Design choice: Implicit is returned rather than Explicit because in general solving for $y$ is required, which is undecidable in symbolic computation. The integration calls crate::integral::integrate, which launches the full integration pipeline (fast table → rational function → Risch → trigonometric rewrite → special functions → heuristic techniques → unevaluated form).

First-Order Linear Equations

Criterion: $y' + p(x),y = q(x)$, where $y$ and $y'$ appear only to the first power.

Algorithm (integrating factor method):

  1. Extract $p(x)$ and $q(x)$ with extract_linear_coeffs
  2. Compute the integrating factor $\mu(x) = e^{\int p,dx}$
  3. General solution: $$y = e^{-\int p,dx}\left(\int e^{\int p,dx},q(x),dx + C_1\right)$$
#![allow(unused)]
fn main() {
let p_integral = integrate(ctx, p, var);
let mu = ctx.fun("exp", &[p_integral]);
let mu_q = ctx.mul(&[mu, q]);
let int_mu_q = integrate(ctx, mu_q, var);
let neg_p_integral = ctx.mul(&[ctx.num(-1), p_integral]);
let exp_neg_p = ctx.fun("exp", &[neg_p_integral]);
let particular = ctx.mul(&[exp_neg_p, int_mu_q]);
let c1 = ctx.var("C1");
let homogeneous = ctx.mul(&[c1, exp_neg_p]);
}

extract_linear_coeffs decomposes the equation into additive terms and classifies them as $y'$-terms, $y$-terms, and free terms. For the $y$-terms it extracts the coefficient $p(x)$ by filtering out the func factor.

Bernoulli Equations

Criterion: $y' + p(x),y = q(x),y^n$ ($n \neq 0, 1$).

Algorithm:

  1. Find $n$ in $y^n$ recursively with find_bernoulli_power
  2. Substitute $v = y^{1-n}$; the equation becomes $v' + (1-n),p(x),v = (1-n),q(x)$
  3. Solve for $v$ with the linear-equation method
  4. Substitute back $y = v^{1/(1-n)}$

find_power_inner walks the AST recursively and returns $n$ when it encounters Pow(base, Num(n)) whose base contains func.

Edge cases: $n = 0$ or $n = 1$ degenerates to a linear equation; None is returned so that LinearFirst handles it.

Exact Equations

Criterion: $M(x,y) + N(x,y),y' = 0$ with $\partial M/\partial y = \partial N/\partial x$.

Algorithm:

  1. Split into $M$ and $N$ with split_mn ($y'$ must appear linearly, otherwise None is returned)
  2. Check exactness (partials_equal)
  3. If not exact, try an integrating factor (find_integrating_factor)
  4. Integrate $M$ with respect to $x$ to obtain part of the potential $F$
  5. Compute the correction term $g(y) = N - \partial F/\partial y$
  6. If $g(y)$ does not contain $x$, integrate with respect to $y$ to obtain the full solution $F + g = C$

Integrating factor strategies:

  • Candidate 1: $(M_y - N_x)/N$ contains only $x$ → $\mu(x) = \exp!\int!(M_y - N_x)/N,dx$
  • Candidate 2: $(N_x - M_y)/M$ contains only $y$ → $\mu(y) = \exp!\int!(N_x - M_y)/M,dy$

The helper exp_simplify simplifies $\exp(k \cdot \log u) = u^k$, so that the integrating factor does not remain in unsimplified exponential form.

Homogeneous Equations

Criterion: $y' = f(y/x)$ (all additive terms have the same total $(x,y)$ degree).

Algorithm:

  1. Substitute $v = y/x$ (i.e. $y = vx$), obtaining $y' = v + xv'$
  2. The equation becomes $v + xv' = f(v)$, i.e. $xv' = f(v) - v$ (separable in $v$ and $x$)
  3. Split into $v$-terms and $x$-terms with separate_by_var
  4. Integrate both sides, then substitute back $v = y/x$

Returns an Implicit solution.


Second-Order Linear ODE Solvers

The second-order solvers are implemented in second_order.rs.

Constant-Coefficient Equations

Form: $a,y'' + b,y' + c,y = f(x)$, where $a, b, c$ are constants.

Algorithm:

  1. Extract $a, b, c$ and $f(x)$ with extract_second_order_coeffs
  2. Verify that $a, b, c$ do not contain $x$ (contains_x)
  3. Compute the discriminant $\Delta = b^2 - 4ac$ of the characteristic equation $ar^2 + br + c = 0$
  4. Construct the homogeneous solution $y_c$
  5. Construct a particular solution $y_p$ (undetermined coefficients or variation of parameters)
  6. Return $y = y_c + y_p$

Three Cases for the Homogeneous Solution

constant_coeff_basis constructs the fundamental solution set from the discriminant:

DiscriminantFundamental solution set
$\Delta > 0$ (two real roots $r_1, r_2$)$e^{r_1 x},; e^{r_2 x}$
$\Delta = 0$ (repeated root $r$)$e^{rx},; x,e^{rx}$
$\Delta < 0$ (complex roots $\alpha \pm \beta i$)$e^{\alpha x}\cos\beta x,; e^{\alpha x}\sin\beta x$

In the implementation, the discriminant branches directly once it has been collapsed (collect_terms) to Num(d). For complex roots, $\beta = \sqrt{-\Delta}/(2a)$: when $-\Delta$ is a perfect square, $\beta$ is computed exactly as a rational number; otherwise the symbolic radical is kept.

#![allow(unused)]
fn main() {
let sn = isqrt(num);
if sn * sn == num {
    // beta = sn/den — exact rational
    ctx.mul(&[ctx.num(sn), ctx.pow(ctx.num(den), ctx.num(-1))])
} else {
    // beta = sqrt(num)/den — symbolic form
    ctx.mul![
        ctx.pow(ctx.num(num), ctx.pow(ctx.num(2), ctx.num(-1))),
        ctx.pow(ctx.num(den), ctx.num(-1)),
    ]
}
}

The Method of Undetermined Coefficients

particular_solution_undetermined dispatches on the type of the forcing term:

Forcing typeDetectionSolving strategy
Polynomial $f(x)$is_polynomial_inset $y_p = \sum A_k x^{k+s}$, substitute back, solve for the coefficients
Exponential $F e^{kx}$extract_exp_forcingset $y_p = A x^s e^{kx}$
Trigonometric $f_c\cos\omega x + f_s\sin\omega x$extract_trig_forcingset $y_p = x^s(A\cos\omega x + B\sin\omega x)$

$s$ is the resonance shift: when $k$ (or $0$) is an $s$-fold root of the characteristic equation, the trial solution is multiplied by $x^s$.

Back-substitution algorithm for polynomial forcing:

For $y_p = \sum_{k=0}^{d} A_k x^{k+s}$, substituting into $ay'' + by' + cy = f(x)$ yields recurrence relations from the coefficients of equal powers of $x$. Solve backwards from the highest power $k = d$:

  • $s = 0$ (no resonance): $A_k = \frac{f_k - b(k+1)A_{k+1} - a(k+2)(k+1)A_{k+2}}{c}$
  • $s = 1$ (simple resonance): $A_k = \frac{f_k - a(k+2)(k+1)A_{k+1}}{b(k+1)}$
  • $s = 2$ (double resonance): $A_k = \frac{f_k}{a(k+2)(k+1)}$

Superposition principle: each additive term of the polynomial forcing is solved separately and the results are summed.

Variation of Parameters

When the method of undetermined coefficients does not apply, it falls back to variation of parameters (variation_of_parameters):

Given the homogeneous solutions $y_1, y_2$ and the standard-form equation $y'' + py' + qy = g$:

  1. Compute the Wronskian $W = y_1 y_2' - y_1' y_2$
  2. $u_1' = -y_2 g / W$, $u_2' = y_1 g / W$
  3. Integrate to obtain $u_1, u_2$
  4. $y_p = y_1 u_1 + y_2 u_2$

If the integration returns the unevaluated Integral(...) form, the method fails (is_integral_fallback).

Cauchy–Euler Equations

Form: $a x^2 y'' + b x y' + c y = f(x)$.

Algorithm:

  1. Extract the coefficients with extract_cauchy_euler_coeffs
  2. Reduce to constant coefficients (divide by the powers of $x$)
  3. Solve the indicial equation $ar(r-1) + br + c = 0$, i.e. $ar^2 + (b-a)r + c = 0$
  4. Construct the homogeneous solution from the discriminant:
DiscriminantFundamental solution set
$\Delta > 0$ (two real roots $r_1, r_2$)$x^{r_1},; x^{r_2}$
$\Delta = 0$ (repeated root $r$)$x^r,; x^r \ln x$
$\Delta < 0$ (complex roots $\alpha \pm \beta i$)$x^\alpha\cos(\beta\ln x),; x^\alpha\sin(\beta\ln x)$
  1. For the inhomogeneous term, find a particular solution by variation of parameters (divide by $ax^2$ to get standard form)

Reduction of Order

Applies to: any second-order linear ODE (constant coefficients not required).

Algorithm:

  1. Try candidate solutions $y_1 \in {1, x, x^2, e^x, e^{-x}, e^{2x}}$
  2. Verify $a y_1'' + b y_1' + c y_1 = 0$ (satisfies_extracted)
  3. If $y_1$ satisfies the homogeneous equation, the second solution is: $$y_2 = y_1 \int \frac{e^{-\int p,dx}}{y_1^2},dx$$ where $p = b/a$
  4. The particular solution is obtained by variation of parameters
  5. Return $y = C_1 y_1 + C_2 y_2 + y_p$

Design choice: the candidate set ${1, x, x^2, e^x, e^{-x}, e^{2x}}$ covers the simple solution forms most common in teaching and engineering. For more general equations, the fallback is PowerSeries.


The Laplace Transform Method

laplace.rs implements Laplace-transform solving of initial-value problems, invoked through the dsolve_ivp entry point.

Applicability

  • First order: $a y' + b y = f(x)$, $y(0) = y_0$
  • Second order: $a y'' + b y' + c y = f(x)$, $y(0) = y_0$, $y'(0) = y_1$
  • The coefficients $a, b, c$ must be integer constants
  • The forcing term $f(x)$ must be in the transform table

The Forward Transform

laplace_kernel transforms the $x$-dependent kernel functions. The supported kernels:

KernelLaplace transform
$1$ (constant)$1/s$
$x$$1/s^2$
$x^n$ ($n \leq 12$)$n!/s^{n+1}$
$e^{kx}$$1/(s-k)$
$\sin\omega x$$\omega/(s^2 + \omega^2)$
$\cos\omega x$$s/(s^2 + \omega^2)$
$e^{kx}\sin\omega x$$\omega/((s-k)^2 + \omega^2)$
$e^{kx}\cos\omega x$$(s-k)/((s-k)^2 + \omega^2)$

split_const_factors splits a term into an $x$-independent constant and an $x$-dependent kernel, transforming only the kernel part. linear_coeff_of extracts $k$ from $kx$ (for $x$ itself it returns $1$).

Building the Algebraic Equation

The transformed algebraic equation:

First order: $a(sY - y_0) + bY = F(s)$ $$Y(s) = \frac{F(s) + a,y_0}{as + b}$$

Second order: $a(s^2Y - sy_0 - y_1) + b(sY - y_0) + cY = F(s)$ $$Y(s) = \frac{F(s) + a(sy_0 + y_1) + by_0}{as^2 + bs + c}$$

The Inverse Transform

inverse_laplace_term performs partial fraction decomposition and table-lookup inversion of the rational function $Y(s) = \frac{n_1 s + n_0}{as^2 + bs + c}$.

First compute the discriminant $\Delta = b^2 - 4ac$; there are three cases depending on the roots:

Case 1: two distinct real roots $r_1 \neq r_2$

$$\frac{n_1 s + n_0}{(s - r_1)(s - r_2)} = \frac{A}{s - r_1} + \frac{B}{s - r_2}$$

with $A = \frac{n_1 r_1 + n_0}{r_1 - r_2}$ and $B = \frac{n_1 r_2 + n_0}{r_2 - r_1}$.

Inverse transform: $A,e^{r_1 x} + B,e^{r_2 x}$.

Case 2: repeated root $r$ ($\Delta = 0$)

$$\frac{n_1 s + n_0}{(s - r)^2} = \frac{n_1}{s - r} + \frac{n_1 r + n_0}{(s - r)^2}$$

Inverse transform: $n_1,e^{rx} + (n_1 r + n_0),x,e^{rx}$.

Case 3: complex roots $k \pm i\omega$ ($\Delta < 0$)

$$\frac{n_1 s + n_0}{(s - k)^2 + \omega^2} = e^{kx}!\left[n_1\cos\omega x + \frac{n_0 + kn_1}{\omega}\sin\omega x\right]$$

The $e^{kx}$ factor is dropped when $k = 0$. Requires $(n_0 + kn_1) \bmod \omega = 0$ (integer arithmetic).

Integer-Arithmetic Constraints

The Laplace module uses i64 integer arithmetic throughout (via const_i64, quadratic_coeffs_i64, linear_coeffs_i64). This restricts the applicability — equations whose coefficients contain symbolic constants or irrational numbers fall back to other methods. Design rationale: integer arithmetic is completely exact and introduces no floating-point error.


Power Series and the Frobenius Method

series.rs implements the two series methods, used as the fallback when every other method fails.

The Power Series Method

Applies to: solutions of linear ODEs near an ordinary point $x_0$.

Algorithm (solve_power_series):

  1. Construct symbolic coefficients $a_0, a_1, \ldots, a_{N-1}$
  2. Manually construct the series $S = \sum a_n (x - x_0)^n$ together with its first and second derivatives $S', S''$ (build_series_triple — diff is not used, to avoid differentiating the $a_n$)
  3. Substitute into the ODE: $R(x) = a,S'' + b,S' + c,S - f = 0$
  4. Satisfy order by order: for $k = 0, 1, \ldots$, compute $R^{(k)}(x_0) = 0$
  5. Each condition determines one new coefficient $a_{k+\text{order}}$ linearly
  6. Solved coefficients are substituted back immediately

Key implementation details:

  • substitute_series recursively replaces all $y(x)$, $y'(x)$, $y''(x)$ in the equation by the series forms
  • solve_linear_coeff solves $a_n$ from the condition equation $\text{coeff} \cdot a_n + \text{rest} = 0$
  • The free parameters $a_0, \ldots, a_{\text{order}-1}$ remain symbolic (initial conditions)
  • If a condition contains no unknown coefficient but is nonzero, $x_0$ is not an ordinary point → return None and hand over to Frobenius

Limitations: by default $N = 8$ terms. The coefficients are rational expressions in the initial values (not numerical).

The Frobenius Method

Applies to: solutions of second-order linear ODEs near a regular singular point $x_0 = 0$.

Algorithm (solve_frobenius):

  1. Extract the coefficients $a(x), b(x), c(x)$ and verify $f = 0$ (homogeneous equations only)
  2. Set $y = x^r \sum a_n x^n$ and construct $y, y', y''$
  3. Substitute into the ODE and group the residual by powers of $x$
  4. The lowest-power group yields the indicial equation $Ar^2 + Br + C = 0$
  5. Find rational roots of the indicial equation (indicial_coeffs → isqrt + discriminant)
  6. Take the larger root $r_1$ and recurse through the subsequent groups to determine the $a_n$

Construction of $y$:

$$y = u \cdot S, \quad y' = r x^{-1} u S + u S', \quad y'' = r(r-1)x^{-2}uS + 2rx^{-1}uS' + uS''$$

where $u = x^r$ is an opaque placeholder and $S = \sum a_n x^n$. strip_x_and_u strips the powers of $x$ and the $u$ factor from the residual terms, which is used for grouping.

Extraction of the indicial equation (indicial_coeffs):

The terms of the lowest-power group should be $a_0$ times a quadratic in $r$. Extract $A, B, C$ so that the group equals $a_0(Ar^2 + Br + C)$. expand_indicial handles expansions containing the factor $(r-1)$.

Limitations:

  • Only $x_0 = 0$ is supported
  • Homogeneous equations only
  • Only real rational roots
  • Only the series for the larger root is taken (the smaller root may give a linearly independent solution)

$2\times2$ Linear ODE Systems

systems.rs implements solving of constant-coefficient linear systems, invoked through the dsolve_system entry point.

Input Format

The system $\mathbf{Y}' = A\mathbf{Y}$ is given as two equations: $$\text{Derivative}(y_i, x) - (a_{i1}y_1 + a_{i2}y_2) = 0$$

extract_coeff extracts the $2\times2$ coefficient matrix $A$ from each equation.

The Characteristic Polynomial

$$\lambda^2 - \text{tr}(A)\lambda + \det(A) = 0$$

with $\text{tr} = a_{11} + a_{22}$ and $\det = a_{11}a_{22} - a_{12}a_{21}$.

The discriminant is $\Delta = \text{tr}^2 - 4\det$.

Solving the Three Cases

Distinct Real Eigenvalues

$\lambda_1 \neq \lambda_2$, both integers.

  1. eigenvector solves $(A - \lambda I)\mathbf{v} = 0$: if $a_{12} \neq 0$, take $\mathbf{v} = (a_{12},; \lambda - a_{11})$
  2. General solution: $$\mathbf{Y} = C_1 \mathbf{v}_1 e^{\lambda_1 x} + C_2 \mathbf{v}_2 e^{\lambda_2 x}$$

Repeated Eigenvalues

$\lambda_1 = \lambda_2 = \lambda$.

Subcase A: $A = \lambda I$ (complete matrix): $$\mathbf{Y} = C_1 \mathbf{e}_1 e^{\lambda x} + C_2 \mathbf{e}_2 e^{\lambda x}$$

Subcase B: $A$ is defective:

  1. Find the eigenvector $\mathbf{v}$ (eigenvector)
  2. Find a generalized eigenvector $\mathbf{w}$ satisfying $(A - \lambda I)\mathbf{w} = \mathbf{v}$ (generalized_eigenvector)
  3. General solution: $$\mathbf{Y} = e^{\lambda x}!\left[C_1 \mathbf{v} + C_2(x\mathbf{v} + \mathbf{w})\right]$$

generalized_eigenvector solves the linear system $(a_{11}-\lambda)w_1 + a_{12}w_2 = v_1$ for integer solutions, trying $w_1 = 0$ and $w_1 = 1$.

Complex Eigenvalues

$\lambda = \alpha \pm \beta i$ (requires $\text{tr}$ even and $-\Delta$ a perfect square).

  1. $\alpha = \text{tr}/2$, $\beta = \sqrt{-\Delta}/2$
  2. Real part $p$ and imaginary part $q$ of the eigenvector $(v + iw)$: take $v_1 = a_{12}$, $v_2 = (\alpha - a_{11}) + i\beta$, hence $p = (a_{12},; \alpha - a_{11})$ and $q = (0,; \beta)$
  3. Real fundamental solutions: $$\mathbf{Y}_1 = e^{\alpha x}(p\cos\beta x - q\sin\beta x)$$ $$\mathbf{Y}_2 = e^{\alpha x}(p\sin\beta x + q\cos\beta x)$$

The $e^{\alpha x}$ factor is dropped when $\alpha = 0$, to avoid a redundant exp(0).


Helper Utilities

util.rs provides the infrastructure shared by the ODE module.

Order Detection

ode_order recursively scans the AST for Derivative(func, var, var, ...) nodes and returns the highest order of differentiation.

Linearity Checks

is_linear_in checks that func and its derivatives occur only to the first power in the expression:

  • is_func_first_degree counts the occurrences of func/Derivative(func, ...) inside multiplicative factors
  • A factor containing Pow(func, Num(n)) with $n \geq 2$ means nonlinearity

Collecting Like Terms

collect_terms solves the problem that the simplifier does not collect like terms:

  1. Decompose additive terms into a rational coefficient plus base-exponent factor pairs
  2. Group by base-exponent signature
  3. Sum the rational coefficients within each group
  4. Drop groups whose coefficient is zero

The decomposition supports rational exponents: $x \cdot x^{-1/2} = x^{1/2}$.

Exponential Simplification

exp_simplify handles $\exp(k \cdot \log u) = u^k$:

  • Recognizes exponents of the form Mul(k, Fun("log", [u]))
  • Numerical constant factors are discarded from the $\log$ argument (no effect for ODE integrating factors)
  • Falls back to the literal exp form

Substituting and Verifying Solutions

substitute_solution substitutes a candidate solution $y = \text{sol}$ into the equation:

  • Replace func → sol
  • Replace Derivative(func, var) → diff(sol, var)
  • Replace Derivative(func, var, var) → diff(diff(sol, var), var)

verify_solution (tests only) substitutes and checks whether the residual simplifies to zero.


Design Choices and Trade-offs

Simplification Strategy

The ODE module relies heavily on ocas_rewrite::simplify (rule-based simplification, at most 20 rounds of iteration) and ocas_atom::normalize (structural normalization). Their division of labor:

  • normalize: fast structural equivalence testing (string comparison)
  • simplify: rule-based algebraic simplification (combining, expanding, eliminating)

In exactness detection and solution construction, the two are used alternately to handle equivalences the simplifier cannot discover automatically.

Integration Dependency

The ODE solvers call crate::integral::integrate to compute indefinite integrals. This launches the full integration pipeline (fast table → rational function → Risch → trigonometric rewrite → special functions → heuristic techniques → unevaluated form). When the integral cannot be evaluated in closed form, the Integral(...) form is returned — in that case variation of parameters and reduction of order are marked as failed.

Integer Restrictions

The Laplace transform module strictly uses i64 integer arithmetic and rejects symbolic coefficients. This is because partial fraction decomposition requires exact rational arithmetic, and integers are the safest choice over i64. For initial-value problems requiring symbolic coefficients, one should fall back to the general dsolve plus substitution of the initial conditions.

The Fallback Chain

The priority order returned by the classifier ensures the most specific method is tried first:

LinearFirst → Bernoulli → Separable → Exact → Homogeneous
    → LinearConstantCoeff → CauchyEuler → ReductionOfOrder
        → PowerSeries

PowerSeries is the method of last resort and always appears at the end of the list; its solver tries the power-series method first and falls back to Frobenius internally. dsolve tries them in order until the first success.


Source Locations

FileResponsibility
ocas-calc/src/ode/mod.rsEntry points: dsolve, dsolve_ivp, dsolve_system, ODE, ODESolution
ocas-calc/src/ode/classify.rsClassifier: classify_ode, ODEType, the detection functions
ocas-calc/src/ode/first_order.rsFirst-order solvers: separable, linear, Bernoulli, exact, homogeneous
ocas-calc/src/ode/second_order.rsSecond-order solvers: constant coefficient, Cauchy–Euler, reduction of order, variation of parameters
ocas-calc/src/ode/laplace.rsLaplace transform: forward transform table, inverse transform (three root types), IVP solving
ocas-calc/src/ode/series.rsPower series and Frobenius methods
ocas-calc/src/ode/systems.rs$2\times2$ systems: eigenvalue decomposition, real/complex/repeated-root cases
ocas-calc/src/ode/util.rsUtilities: order, linearity, like-term collection, exponential simplification, solution substitution

See Also

Solvers

oCAS provides solvers for linear systems, Diophantine equations, and polynomial systems. This chapter covers the available solvers and their usage.


Linear systems over ℚ

solve_linear_rational solves an $n \times n$ system $Ax = b$ over the rational numbers. Input coefficients are i64 values; the solution is returned as (numerator, denominator) pairs.

#![allow(unused)]
fn main() {
let a = vec![vec![2, 1], vec![1, -1]];
let b = vec![5, 1];
let x = solve_linear_rational(&a, &b).unwrap();
// x = [(2, 1), (1, 1)]  → 2, 1
}

Errors: EmptySystem, NonSquare, Inconsistent, Underdetermined { rank }.

Python:

print(ocas.solve_linear_rational([[2, 1], [1, -1]], [5, 1]))
# [(2, 1), (1, 1)]

Linear systems over ℤ

solve_linear_integer finds integer solutions to $Ax = b$. It returns an error if no integer solution exists.

#![allow(unused)]
fn main() {
// 2x + y = 3
let a = vec![vec![2, 1]];
let b = vec![3];
let x = solve_linear_integer(&a, &b).unwrap();
// x = [1, 1]  (2·1 + 1·1 = 3)
}

Errors include ResultNotInDomain when the solution involves fractions.


Diophantine equations

solve_diophantine solves the linear Diophantine equation $a \cdot x + b \cdot y = c$ for integer $x, y$.

#![allow(unused)]
fn main() {
let sol = solve_diophantine(3, 5, 1).unwrap();
// sol = DiophantineSolution { x0: 2, y0: -1, x_step: 5, y_step: -3 }
}

The result gives a particular solution $(x_0, y_0)$ and step values. The general solution is:

$$ \begin{aligned} x &= x_0 + x_{step} \cdot t \ y &= y_0 + y_{step} \cdot t \end{aligned} $$

for any integer $t$.


Polynomial systems (via Gröbner bases)

solve_polynomial_system solves systems of polynomial equations: it computes a Gröbner basis (default F4; the Algorithm enum selects Buchberger/F4/F5), then performs Sturm real-root isolation and back-substitution for zero-dimensional ideals.

#![allow(unused)]
fn main() {
use ocas_domain::{Rational, RationalDomain};
use ocas_poly::groebner::Algorithm;
use ocas_poly::ideal::solve_polynomial_system;
use ocas_poly::sparse::{Lex, SparseMultivariatePolynomial};

let d = RationalDomain;
// x + y = 0, x*y - 1 = 0
let eq1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(1, 1)),
]);
let eq2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 1], Rational::new(1, 1)),
    (vec![0, 0], Rational::new(-1, 1)),
]);

let sol = solve_polynomial_system(&[eq1, eq2], Algorithm::Auto);
}

The result is a PolynomialSystemSolution: a list of real solutions (ZeroDimSolutions) for zero-dimensional ideals, the Gröbner basis (PositiveDimensional) for positive-dimensional ones, or Empty when no solution exists.

Ideal operations (since 0.23.0)

Ideal arithmetic is provided by the ocas_poly::ideal and ocas_poly::groebner modules:

OperationFunctionDescription
Membershipideal_contains(gens, f, algo)Test if $f \in I$
Sumideal_sum(I, J)$I + J$
Productideal_product(I, J)$I \cdot J$
Quotientideal_quotient(I, J)$I : J$
Saturationideal_saturate(I, J)$I : J^\infty$
Intersectionideal_intersection(I, J)$I \cap J$
Eliminationeliminate(gens, elim_vars, algo) (ocas_poly::groebner)Eliminate variables
Radicalideal_radical(gens)$\sqrt{I}$
Primary decompositionprimary_decomposition(gens)Decompose into primary components
Hilbert serieshilbert_series(&gb) (ocas_poly::groebner::hilbert)Compute Hilbert series
#![allow(unused)]
fn main() {
use ocas_domain::{Rational, RationalDomain};
use ocas_poly::groebner::Algorithm;
use ocas_poly::ideal::*;
use ocas_poly::sparse::{Lex, SparseMultivariatePolynomial};

let d = RationalDomain;
let circle = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![2, 0], Rational::new(1, 1)),
    (vec![0, 2], Rational::new(1, 1)),
    (vec![0, 0], Rational::new(-1, 1)),
]);
let line = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
    (vec![1, 0], Rational::new(1, 1)),
    (vec![0, 1], Rational::new(-1, 1)),
]);

// Solve circle ∩ line
let sol = solve_polynomial_system(&[circle, line], Algorithm::Auto);
}

Ordinary differential equations

dsolve solves ordinary differential equations analytically. The ODE is given as an expression equal to zero, the unknown function (e.g. y(x)), and the independent variable (e.g. x).

#![allow(unused)]
fn main() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
// y' - y = 0
let ode = ODE {
    equation: ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]),
    func: y,
    var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
// ODESolution::Explicit(C1*exp(x))
}

classify_ode returns the applicable solving methods without solving:

#![allow(unused)]
fn main() {
let types = classify_ode(&ctx, ode);
// [LinearFirst, Separable, Homogeneous, PowerSeries]
}

Supported ODE types

TypeFormMethod
Separable$f(x)dx = g(y)dy$Direct integration
Linear first-order$y' + p(x)y = q(x)$Integrating factor $\mu = e^{\int p}$
Bernoulli$y' + p(x)y = q(x)y^n$Substitution $v = y^{1-n}$
Exact$M dx + N dy = 0$, $\partial M/\partial y = \partial N/\partial x$Potential function
Homogeneous$y' = f(y/x)$Substitution $v = y/x$
Integrating factorsnon-exact first-order$\mu(x)$ or $\mu(y)$ detection
Constant coefficients$ay'' + by' + cy = f(x)$Characteristic equation
Cauchy-Euler$ax^2y'' + bxy' + cy = f(x)$Indicial equation
Reduction of ordersecond-order linear$y_2 = y_1\int e^{-\int p}/y_1^2$
Variation of parameterssecond-order non-homogeneousWronskian formula
Undetermined coefficientspolynomial/exponential/trigonometric forcingCoefficient matching + resonance
Power seriesordinary pointCoefficient recursion
Frobeniusregular singular pointIndicial equation + recursion
Laplace IVPfirst/second-order linear IVPdsolve_ivp
2×2 systems$\mathbf{Y}' = A\mathbf{Y}$dsolve_system (eigen-decomposition)

ODEs that cannot be solved analytically are returned as unevaluated ODE(equation, func) forms.

Initial value problems

dsolve_ivp solves linear constant-coefficient IVPs via the Laplace transform:

#![allow(unused)]
fn main() {
// y' - y = 0, y(0) = 1  =>  y = exp(x)
let sol = dsolve_ivp(&ctx, ode, ctx.num(1), None);
}

Systems

dsolve_system solves 2×2 constant-coefficient systems $\mathbf{Y}' = A\mathbf{Y}$:

#![allow(unused)]
fn main() {
// y1' = y2, y2' = -y1 (harmonic oscillator)
let sol = dsolve_system(&ctx, &[eq1, eq2], &[y1, y2], Symbol::new("x"));
// ODESolution::System([C1*sin(x) + C2*cos(x), C1*cos(x) - C2*sin(x)])
}

Python

import ocas

e = ocas.Expression("Derivative(y(x), x) - y(x)")
print(ocas.classify_ode(e, "y", "x"))     # ['LinearFirst', 'Separable', ...]
print(ocas.dsolve(e, "y", "x"))            # y = C1*exp(x)
print(ocas.dsolve_ivp(e, "y", "x", "1"))   # y = exp(x)

C

int err = 0;
char *types = ocas_ode_classify("Derivative(y(x), x) - y(x)", "y", "x", &err);
char *sol = ocas_ode_dsolve("Derivative(y(x), x) - y(x)", "y", "x", NULL, &err);
char *ivp = ocas_ode_dsolve_ivp("Derivative(y(x), x) - y(x)", "y", "x", "1", NULL, &err);
ocas_string_free(types);
ocas_string_free(sol);
ocas_string_free(ivp);

Errors

The linear solvers return Result<T, SolveError>; solve_diophantine returns Option<DiophantineSolution> (None when no solution exists); and solve_polynomial_system returns PolynomialSystemSolution. Common error variants:

ErrorMeaning
EmptySystemNo equations provided
NonLinearSystem is not linear in the requested variables
NonSquareNumber of equations ≠ number of unknowns
InconsistentNo solution exists
Underdetermined { rank }Infinitely many solutions
ResultNotInDomainSolution contains fractions when integers are required

See also

Rewrite & Simplification

oCAS provides a pattern-matching engine and a rule-based simplifier for symbolic expressions. This chapter covers the core concepts and APIs.


Patterns

A Pattern describes an expression structure with wildcards. Three wildcard levels are available, triggered by the wildcard name suffix:

LevelName suffixMatches
WildcardLevel::Single_ (e.g. x_)Any single sub-expression
WildcardLevel::Sequence__ (e.g. __x)One or more operands in an ordered list
WildcardLevel::NullSequence___ (e.g. ___x)Zero or more operands in an ordered list
#![allow(unused)]
fn main() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// Pattern: x + y_  where y_ matches any single sub-expression
let x = ctx.var("x");
let pat = Pattern::Add(vec![
    Pattern::Literal(x),
    Pattern::Wildcard(Symbol::new("y_"), WildcardLevel::Single),
]);

// Match: x + 5  →  binds y_ to 5
let e = parse(&ctx, "x + 5").unwrap();
let bindings = match_pattern(pat, e).unwrap();
match bindings.get(Symbol::new("y_")).unwrap() {
    MatchValue::Single(v) => assert_eq!(v.to_string(), "5"),
    _ => {}
}
}

Within Add and Mul, matching is associative and commutative: arguments are sorted and matched in a canonical order.


Match & Bindings

match_pattern returns Result<Bindings, MatchError>. Bindings maps wildcard Symbol names to matched values.

#![allow(unused)]
fn main() {
// Pattern: a_ + b_ + ___rest  — capture two terms and the rest
let pat = Pattern::Add(vec![
    Pattern::Wildcard(Symbol::new("a_"), WildcardLevel::Single),
    Pattern::Wildcard(Symbol::new("b_"), WildcardLevel::Single),
    Pattern::Wildcard(Symbol::new("___rest"), WildcardLevel::NullSequence),
]);

let e = parse(&ctx, "x + y + z + 5").unwrap();
let bindings = match_pattern(pat, e).unwrap();

// Bindings use MatchValue::Single(atom) or MatchValue::Sequence(slice)
use ocas_rewrite::MatchValue;
match bindings.get(Symbol::new("a_")).unwrap() {
    MatchValue::Single(a) => println!("a = {}", a),  // e.g. "x" or "5"
    _ => {}
}
}

MatchError variants:

  • NoMatch — pattern does not match
  • InconsistentBinding — same wildcard name bound to different values
  • BudgetExhausted — backtrack budget exhausted

Rules

A Rule pairs a pattern with a replacement closure that receives the match bindings and the arena context.

#![allow(unused)]
fn main() {
use ocas_rewrite::rules::default_rules;

// Built-in rule set
let rules = default_rules(&ctx, &());

// Custom rule: x_ + 0 → x_
let custom_pat = Pattern::Add(vec![
    Pattern::Wildcard(Symbol::new("x_"), WildcardLevel::Single),
    Pattern::Literal(ctx.num(0)),
]);
let custom_rule = Rule::new(custom_pat, |bindings, _ctx| {
    match bindings.get(Symbol::new("x_")).unwrap() {
        MatchValue::Single(x) => *x,
        _ => unreachable!(),
    }
});
}

Simplification

simplify() applies a rule set repeatedly until a fixed point (or max iterations) is reached.

#![allow(unused)]
fn main() {
let e = parse(&ctx, "x + 0 + y*0 + z*1").unwrap();
let rules = default_rules(&ctx, &());
let simplified = simplify(&ctx, e, &rules, 20);
println!("{}", simplified);  // x + z
}

The default rule set handles:

  • Identity removal: x + 0 → x, x * 1 → x, x * 0 → 0
  • Constant folding: 2 + 3 → 5, 2 * 3 → 6
  • Power simplifications: x^0 → 1, x^1 → x, 0^x → 0, 1^x → 1
  • Arithmetic on numbers: 2*3 + 4*5 → 26

Bottom-up transformation

transform() walks the expression tree bottom-up, applying a function to each node. This is useful for custom traversals that do not fit the pattern-matching model.

#![allow(unused)]
fn main() {
// Replace every variable "x" with "t"
let replacer = |_ctx: &AtomArena, atom: Atom| {
    if let AtomNode::Var(sym) = _ctx.get(atom) {
        if sym.as_str() == "x" {
            return _ctx.var("t");
        }
    }
    atom
};

let e = parse(&ctx, "x^2 + x + 1").unwrap();
let result = transform(&ctx, e, &replacer);
println!("{}", result);  // t^2 + t + 1
}

E-graph simplification (egg feature)

With the egg feature enabled, oCAS can use equality saturation for more powerful simplifications that rule-based rewriting alone cannot achieve.

cargo build -p ocas --features egg
#![allow(unused)]
fn main() {
// Requires `egg` feature
#[cfg(feature = "egg")]
{
    use ocas_rewrite::egraph::egg_simplify;
    let e = parse(&ctx, "sin(x)^2 + cos(x)^2").unwrap();
    let result = egg_simplify(&ctx, e).unwrap();
    println!("{}", result);  // 1
}
}

The e-graph approach explores multiple equivalent forms simultaneously, combining rewrites via congruence closure. This handles trigonometric identities and algebraic equalities that require multiple rewrite steps in a specific order.


Fuel-bounded simplification

simplify_with_fuel is a variant of simplify that consumes one fuel unit per bottom-up traversal pass and stops early when the budget is exhausted. This is useful when processing untrusted or pathological expressions that could cause the rewriter to spin indefinitely.

#![allow(unused)]
fn main() {
use ocas_core::fuel::Fuel;
use ocas_rewrite::simplify::simplify_with_fuel;

let fuel = Fuel::new(100);
let result = simplify_with_fuel(&ctx, e, &rules, 20, &fuel);
match result {
    Ok(expr) => println!("simplified: {}", expr),
    Err(_) => println!("fuel exhausted before fixpoint"),
}
}

simplify_with_fuel returns Err only when fuel ran out before a fixpoint was reached. The old simplify API remains available with identical semantics (no fuel limit).


Limitations

The default rule-based simplifier intentionally keeps things predictable:

  • Does not handle sin(x)^2 + cos(x)^2 → 1 (requires the egg feature)
  • Does not perform full polynomial expansion by default
  • Does not apply trigonometric or logarithmic identities

For advanced simplification, enable the egg feature or write custom rules.


See also


Backtracking AC Matching (0.22.0)

The matcher now uses full backtracking search for Add/Mul contexts, replacing the previous greedy algorithm. This enables:

  • Sequence wildcards inside Add/Mul: x_ + __rest matches any sum with at least one captured term.
  • Multiple wildcards in AC: x_ + y_ + z_ against a + b + c + d correctly binds each wildcard to a distinct term.
  • Backtrack budget: prevents pathological blow-up on adversarial patterns (default 10,000 backtrack attempts).

The default match_pattern uses the budget; to adjust:

#![allow(unused)]
fn main() {
use ocas_rewrite::matcher::match_pattern_with_budget;
let bindings = match_pattern_with_budget(pattern, atom, 50_000)?;
}

Multi-pattern Replacement (0.22.0)

ocas_rewrite::replace provides controlled replacements with condition guards and traversal settings.

replace_once / replace_all

#![allow(unused)]
fn main() {
use ocas_rewrite::replace::{replace_once, replace_all};

// Replace the first occurrence of x anywhere in the tree:
let result = replace_once(&ctx, expr, Pattern::Literal(x), |_, ctx| ctx.num(42));

// Replace all occurrences of x:
let result = replace_all(&ctx, expr, Pattern::Literal(x), |_, ctx| ctx.num(42));
}

replace_all_multiple — first-match-wins

#![allow(unused)]
fn main() {
use ocas_rewrite::replace::{replace_all_multiple, Replacement};

let replacements = vec![
    Replacement { pattern: pat1, replacement: rhs1, condition: None },
    Replacement { pattern: pat2, replacement: rhs2, condition: Some(cond) },
];
let result = replace_all_multiple(&ctx, expr, &replacements);
}

At each node, replacements are tried in order; the first match wins.

Conditions

A Condition is a predicate on Bindings:

#![allow(unused)]
fn main() {
use ocas_rewrite::replace::Condition;

let cond = Condition::new(|bindings| {
    // Only apply when bound atom is a number
    match bindings.get(Symbol::new("x_")) {
        Some(MatchValue::Single(a)) => matches!(a.node(), AtomNode::Num(_)),
        _ => false,
    }
});
}

Transformer::Partition (0.22.0)

Partitions a arg(a, b, c, …) expression into named bins of given capacities, returning a sum over all valid ways to distribute the elements.

#![allow(unused)]
fn main() {
use ocas_rewrite::transformer::partition_expr;
use ocas_atom::Symbol;

// arg(1, 3, 2, 3, 1) with bins [(f,2), (g,2), (f,1)]
let result = partition_expr(&ctx, expr, &[(Symbol::new("f"), 2), (Symbol::new("g"), 2), (Symbol::new("f"), 1)], false, false);
// Returns Σ coeff · f(…)*g(…)*f(…) for all valid partitions.
}

The three modes:

  • exact (both flags false): total bin capacity must equal element count.
  • fill_last: surplus elements absorbed into the last bin.
  • repeat: bin pattern repeated until all elements consumed.

Evaluation & JIT

oCAS provides three paths for numeric evaluation of symbolic expressions: an interpreter (stack VM), a Cranelift JIT compiler, and a SIMD-vectorized batch evaluator. This chapter explains each path and when to use it.


Stack VM interpreter

The default evaluation path. ExpressionEvaluator compiles an Atom expression tree into a sequence of stack-machine instructions, then executes them on a flat operand stack.

#![allow(unused)]
fn main() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// Parse and compile
let e = parse(&ctx, "x^2 + y").unwrap();
let ev = ExpressionEvaluator::<f64>::compile(e).unwrap();

// Evaluate with parameter values
let result = ev.evaluate(&[3.0, 1.0]).unwrap();  // [10.0]
let result = ev.evaluate(&[2.0, 0.0]).unwrap();  // [4.0]
}

The compiler automatically detects free variables, assigns them parameter slots in sorted order, and optimizes the instruction sequence (constant folding, copy-chain removal).

Python:

ev = ocas.ExpressionEvaluator("x^2 + y", ["x", "y"])
print(ev.evaluate([3.0, 1.0]))  # [10.0]
print(ev.evaluate([2.0, 0.0]))  # [4.0]

Cranelift JIT

With the jit feature, oCAS compiles expressions to native machine code via Cranelift. This is ideal for repeatedly evaluating the same expression with different inputs — amortizing the compilation cost over many calls.

cargo build -p ocas --features jit
#![allow(unused)]
fn main() {
#[cfg(feature = "jit")]
{
    use ocas::prelude::*;

    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);
    let e = parse(&ctx, "sin(x) * cos(y)").unwrap();

    let ev: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(e).unwrap();
    let compiled = ev.compile_jit().unwrap();
    let result = compiled.call(&[0.5, 1.0]);  // ~0.2590
}
}

The JIT path translates the same IR used by the interpreter into native x86-64 or aarch64 code. For expressions evaluated thousands of times, this yields up to 97× speedup over the interpreter (see Benchmarks).

Multi-output JIT

compile_multi compiles several expressions into one evaluator, sharing common subexpressions across outputs; call_into writes results into a caller-provided buffer (zero allocation per call).

#![allow(unused)]
fn main() {
#[cfg(feature = "jit")]
{
    use ocas::prelude::*;

    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);
    let e1 = parse(&ctx, "sin(x) + 1").unwrap();
    let e2 = parse(&ctx, "sin(x) * 2").unwrap();

    let ev: ExpressionEvaluator<f64> =
        ExpressionEvaluator::compile_multi(&[e1, e2]).unwrap();
    let compiled = ev.compile_jit().unwrap();
    let mut out = [0.0f64; 2];
    compiled.call_into(&[1.0], &mut out);
}
}

f32 mixed precision

compile_jit_f32 generates single-precision code (libm *f symbols); compile_vector_evaluator_f32 doubles the SIMD lane count on the same hardware. Use when f32 accuracy suffices.


SIMD batch evaluation

The simd feature enables vectorized evaluation using pulp, computing multiple inputs simultaneously with runtime-detected SIMD width (SSE2/AVX2/ AVX-512).

cargo build -p ocas --features simd
#![allow(unused)]
fn main() {
#[cfg(feature = "simd")]
{
    use ocas::prelude::*;

    let arena = Arena::new();
    let ctx = AtomArena::new(&arena);
    let e = parse(&ctx, "x^2 + y").unwrap();

    let ev: ExpressionEvaluator<f64> = ExpressionEvaluator::compile(e).unwrap();
    let vec = ev.compile_vector_evaluator().unwrap();
    // Each vec holds one parameter's values across all input rows
    let results = vec.evaluate(&[
        vec![1.0, 2.0, 3.0, 0.0],   // x: 1² + 0 = 1, 2² + 1 = 5, …
        vec![0.0, 1.0, 2.0, 5.0],   // y
    ]).unwrap();
    // results[0] = [1.0, 5.0, 11.0, 5.0] (one row per output; a single
    // output yields exactly one row)
}
}

SIMD evaluation processes many input rows in parallel (the batch lane count is the runtime-detected SIMD width), ideal for parameter sweeps, plotting, and Monte Carlo workloads.


Comparison

PathFeatureLatencyThroughputBest for
Interpreter(default)Low (no compile overhead)MediumSingle-shot, interactive use
JITjitHigh (compile) + Low (run)HighRepeated evaluation of same expression
SIMDsimdLow–MediumVery High (4×)Batch evaluation, parameter sweeps

When to use each

  • Interpreter: Interactive REPL sessions, one-off evaluations, Python API.
  • JIT: Repetitive numeric evaluation (root-finding iterations, optimization loops).
  • SIMD: Batching many inputs (plotting, sensitivity analysis, ML pipelines).

User-defined functions

FunctionMap allows registering custom Rust functions callable from evaluated expressions.

#![allow(unused)]
fn main() {
let mut fns = FunctionMap::<f64>::new();
fns.insert("f", |args: &[f64]| args[0].sin() * args[1].cos());

let e = parse(&ctx, "f(x, y)").unwrap();
let ev = ExpressionEvaluator::<f64>::compile_with(e, fns).unwrap();
let result = ev.evaluate(&[0.5, 1.0]).unwrap();
}

See also

Numeric Integration

oCAS ships an adaptive Monte Carlo integrator (Vegas) for numerical evaluation of definite integrals. This is useful when a closed-form symbolic antiderivative does not exist or is too expensive to compute.


The Vegas Algorithm

Vegas (Lepage 1978) is an importance-sampling Monte Carlo method that iteratively refines a piecewise-constant approximation of the integrand to concentrate samples where the function varies most. oCAS implements the standard adaptive-grid variant with configurable iteration count, sample budget, and learning rate.

Entry pointDescription
integrate_1d(f, a, b, opts)Convenience wrapper for 1-D integrals
Vegas::new(n_dims, opts)Multi-dimensional integrator
Integrator::integrate(&mut self, f)Run integration on a closure

Results are returned as IntegrateResult { integral, error }.


Quick Start: 1-D Integration

#![allow(unused)]
fn main() {
use ocas_eval::numeric::{integrate_1d, VegasOptions};

// Integrate sin(x) from 0 to π
let opts = VegasOptions {
    n_bins: 50,
    n_samples: 10_000,
    iterations: 5,
    learning_rate: 1.5,
    seed: 42,
};
let result = integrate_1d(|x| x.sin(), 0.0, std::f64::consts::PI, opts);
println!("integral = {:.6} ± {:.6}", result.integral, result.error);
// ≈ 2.000000
}

Multi-Dimensional Integration

For integrals over multiple variables, use Vegas directly:

#![allow(unused)]
fn main() {
use ocas_eval::numeric::{Vegas, VegasOptions, Integrator};

let opts = VegasOptions {
    n_bins: 50,
    n_samples: 10_000,
    iterations: 10,
    learning_rate: 1.5,
    seed: 0,
};
let mut vegas = Vegas::new(2, opts);

// Integrate x*y over [0,1]×[0,1]  → exact result = 0.25
let result = vegas.integrate(&|coords| coords[0] * coords[1]);
println!("integral = {:.6} ± {:.6}", result.integral, result.error);
}

The closure receives a &[f64] slice with one entry per dimension, each already mapped to the unit hypercube [0, 1]ⁿ.


Tuning Parameters

FieldDefaultEffect
n_bins64Number of grid bins per dimension
n_samples10,000Samples per iteration
iterations10Number of adaptive iterations
learning_rate1.5Grid adaptation speed (1.0–2.0)
seed0x0C45 (u64)RNG seed for reproducibility

More iterations and samples reduce the variance estimate but increase runtime. The learning rate controls how aggressively the grid adapts to the integrand structure.


Statistics Accumulator

StatisticsAccumulator is the internal inverse-variance weighted accumulator used by Vegas. It tracks per-iteration integral estimates, chi-square diagnostics, and the final combined result.

#![allow(unused)]
fn main() {
use ocas_eval::numeric::StatisticsAccumulator;

let mut acc = StatisticsAccumulator::new();
acc.add_sample(1.0, 1.5);
acc.add_sample(1.0, 1.8);
acc.add_sample(1.0, 2.1);
acc.finalize_iteration();

println!("integral = {:.6}", acc.integral());
println!("error    = {:.6}", acc.error());
println!("chi²     = {:.2}", acc.chi_square());
}

Python & C Usage

Python

import ocas

# 1-D convenience
result = ocas.integrate_1d(lambda x: x**2, 0, 1, n_samples=10000, iterations=10)
print(result.integral, result.error)

# Multi-dimensional
vegas = ocas.Vegas(n_dims=2, n_samples=10000, iterations=10)
result = vegas.integrate(lambda coords: coords[0] * coords[1])
print(result.integral)

C

#include <ocas.h>

/* 1-D convenience (pass NULL for opts to use the library defaults) */
int err = 0;
struct ocas_OcasIntegrateResult result =
    ocas_integrate_1d(my_fn, NULL, 0.0, 1.0, NULL, &err);
printf("integral = %f ± %f\n", result.integral, result.error);

See the Python API and C/C++ API chapters for full binding documentation.


Limitations

  • Vegas uses Monte Carlo sampling; results are statistical estimates with error bars, not exact symbolic values.
  • Convergence is slow for high-dimensional integrals with strong correlations between variables.
  • The integrand must be a plain f64 → f64 function; symbolic expressions must be compiled to a numeric evaluator first (see Evaluation & JIT).

Automatic Differentiation

oCAS provides forward-mode automatic differentiation via hyper-dual numbers. This is distinct from symbolic differentiation (diff): it evaluates derivatives numerically alongside the function value, without building or simplifying an expression tree.


Hyper-Dual Numbers

A hyper-dual number extends the scalar field with infinitesimal components ε₁, ε₂, … that satisfy εᵢ² = 0. Arithmetic on these numbers propagates exact first (and optionally higher) partial derivatives through every operation.

oCAS represents them as HyperDual<T> over any coefficient type T that implements the DualCoeff trait. In practice T is typically Rational (the only type with a complete DualCoeff implementation in the standard build).

TypeRole
DualShapeLayout descriptor: variable groupings, multiplication table
HyperDual<T>Concrete dual number with value + derivative slots
DualCoeffTrait: zero, one, arithmetic ops for T
new_first_order(n)Convenience: shape with n first-order components

Quick Start

#![allow(unused)]
fn main() {
use ocas_domain::Rational;
use ocas_domain::dual::{new_first_order, HyperDual};

// First-order dual for 2 variables
let shape = new_first_order::<Rational>(2);

// x = 1 + ε₁ (variable 0, unit coefficient)
let x = HyperDual::variable(&shape, 0, Rational::new(1, 1));
// y = 2 + ε₂ (variable 1, unit coefficient)
let y = HyperDual::variable(&shape, 1, Rational::new(2, 1));

// f = x * y  →  value = 2, ∂f/∂x = 2, ∂f/∂y = 1
let f = x * y;
println!("f    = {}", f.value());           // 2
println!("df/dx = {}", f.deriv(0).unwrap()); // 2
println!("df/dy = {}", f.deriv(1).unwrap()); // 1
}

Available Operations

HyperDual<T> supports the standard arithmetic traits (Add, Sub, Mul, Div, Neg). Each operation propagates derivatives via the precomputed multiplication table in DualShape.

OperationSupported?
+, -, *, /Yes
inv() (multiplicative inverse)Yes
pow / exp / log / trigNo (transcendental functions require DualCoeff extensions not yet implemented)

For transcendental functions, fall back to symbolic differentiation (diff) or compile the expression to a numeric evaluator with auto-diff support (planned post-1.0).


Higher-Order Derivatives

DualShape supports multi-index groupings for higher-order derivatives (e.g., second-order εᵢεⱼ components). Use DualShape::new with nested index vectors for custom shapes, or the first-order convenience function for the common case.


Python & C Usage

Python

from ocas import DualShape, HyperDual

shape = DualShape.first_order(2)
x = HyperDual.variable(shape, 0, 1)
y = HyperDual.variable(shape, 1, 2)

f = x * y
print(f.value())    # 2
print(f.deriv(0))   # 2  (∂f/∂x)
print(f.deriv(1))   # 1  (∂f/∂y)

# Arithmetic dunder methods: __add__, __sub__, __mul__, __truediv__, __neg__
g = x + y * x
print(g.deriv(0))   # 3  (1 + y = 1 + 2)

C

#include <ocas.h>

int err = 0;
struct ocas_OcasDualShape *shape = ocas_dual_shape_new(2, &err);
struct ocas_OcasHyperDual *x = ocas_dual_variable(shape, 0, "1", &err);
struct ocas_OcasHyperDual *y = ocas_dual_variable(shape, 1, "2", &err);

struct ocas_OcasHyperDual *f = ocas_dual_mul(x, y, &err);
char *val = ocas_dual_value(f, &err);    /* "2" */
char *dx  = ocas_dual_deriv(f, 0, &err); /* "2" */

ocas_string_free(val);
ocas_string_free(dx);
ocas_hyperdual_free(f);
ocas_hyperdual_free(y);
ocas_hyperdual_free(x);
ocas_dual_shape_free(shape);

See the Python API and C/C++ API chapters for full documentation.


Limitations

  • Only Rational coefficients are supported in the standard build (no floating-point or finite-field duals yet).
  • Transcendental functions (sin, cos, exp, log, pow) are not yet available on HyperDual.
  • JIT compilation of dual expressions is not yet integrated.

Tensors

oCAS provides basic tensor algebra with explicit index management. A Tensor carries named index slots (upper/lower), optional symmetry metadata, and can be converted to an Atom expression for symbolic processing.


Index Slots

Each index on a tensor has:

PropertyTypeMeaning
labelAtomIndex name (typically a Greek letter or i, j, …)
positionIndexPositionUpper (contravariant) or Lower (covariant)
#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;
use ocas_atom::AtomArena;
use ocas_atom::tensor::{IndexSlot, IndexPosition};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

let mu = IndexSlot::new(ctx.var("mu"), IndexPosition::Upper);
let nu = IndexSlot::new(ctx.var("nu"), IndexPosition::Lower);
}

Creating Tensors

A Tensor is defined by a name (Symbol) and a vector of index slots:

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;
use ocas_atom::{AtomArena, Symbol};
use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition, Symmetry};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

let slots = vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
    IndexSlot::new(ctx.var("nu"), IndexPosition::Lower),
];
let t = Tensor::new(Symbol::new("g"), slots);

println!("rank = {}", t.rank());       // 2
println!("name = {}", t.name());       // g
}

Optionally attach symmetry metadata:

#![allow(unused)]
fn main() {
let symmetric = t.clone().with_symmetry(Symmetry::Symmetric);
let antisymmetric = t.clone().with_symmetry(Symmetry::Antisymmetric);
}

Symmetry metadata is advisory for downstream consumers; the tensor algebra operations do not automatically symmetrize.


Contraction

contract takes two tensors and returns either a scalar (if all indices contract) or a TensorProduct with the remaining free indices:

#![allow(unused)]
fn main() {
use ocas_core::arena::Arena;
use ocas_atom::{AtomArena, Symbol};
use ocas_atom::tensor::{contract, Contracted, IndexSlot, IndexPosition, Tensor};

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

// Contract two rank-1 tensors: A^μ B_μ → scalar
let a = Tensor::new(Symbol::new("A"), vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Upper),
]);
let b = Tensor::new(Symbol::new("B"), vec![
    IndexSlot::new(ctx.var("mu"), IndexPosition::Lower),
]);

match contract(&ctx, &a, &b) {
    Contracted::Scalar(expr) => println!("scalar: {}", expr),
    Contracted::Product(tp) => {
        for f in &tp.factors {
            println!("free factor: {}", f.name());
        }
    }
}
}

Contraction matches indices by label and opposite position. Multiple pairs of matching indices are contracted in a single call.


Symmetrization

symmetrise_sign returns the sign of the antisymmetrization of a tensor: +1 for even permutations, -1 for odd permutations of the dummy (contracted) labels. This is useful for computing determinant-like expressions.

#![allow(unused)]
fn main() {
use ocas_atom::tensor::symmetrise_sign;

let sign = symmetrise_sign(&tensor);
}

Converting to Atoms

A tensor can be lowered to a standard Atom expression via to_atom:

#![allow(unused)]
fn main() {
let atom = tensor.to_atom(&ctx);
println!("{}", atom);  // e.g. g(mu, nu)
}

This allows tensors to participate in the general symbolic pipeline (rewriting, simplification, differentiation).


Python & C Usage

Python

from ocas import Tensor, contract_tensors, tensor_symmetrise_sign

# Create a rank-2 tensor
g = Tensor("g", [("mu", "upper"), ("nu", "lower")])
print(g.rank())    # 2
print(g.name())    # g

# Contract two tensors
A = Tensor("A", [("mu", "upper")])
B = Tensor("B", [("mu", "lower")])
result = contract_tensors(A, B)
print(result)      # scalar expression

# Antisymmetrization sign
sign = tensor_symmetrise_sign(g)

C

#include <ocas.h>

int err = 0;
/* Create tensor A^μ (slots is a "label,position;..." string; symmetry may be NULL) */
struct ocas_OcasTensor *A = ocas_tensor_create("A", "mu,upper", NULL, &err);

/* Query rank */
size_t rank = ocas_tensor_rank(A);  /* 1 */

ocas_tensor_free(A);

See the Python API and C/C++ API chapters for full documentation.


Limitations

  • Tensor algebra is explicit: indices are matched by label, not by Einstein summation convention. You must call contract manually.
  • The Tensor Symmetry metadata supports only Symmetric and Antisymmetric; finer-grained symmetries (subset symmetries symmetric_subsets, cyclic cyclic) are expressed through the 0.22.0 SymmetrySpec/TensorRegistry in graph canonicalization.
  • Graph canonicalization requires registering each tensor's symmetry spec in a TensorRegistry first; unregistered tensors are treated as having no symmetry.
  • Graph-based canonicalization does not perform physics-level semantics such as automatic symmetry enforcement in Wick contractions.

Tensor Canonicalisation (0.22.0)

Graph-isomorphism-based canonicalisation provides label-independent unique canonical forms for tensor expressions. Two tensor products that differ only by index relabelling produce the same canonical form.

How it works

  1. Encode the tensor expression as a graph: tensor heads become head vertices, each index slot becomes a slot vertex, contractions become undirected edges, and symmetries are encoded as hidden positional data.
  2. Run McKay refinement-individualisation (1-WL colour refinement + individualisation DFS with automorphism-orbit pruning) on the graph.
  3. Reconstruct the canonical expression from the canonical labelling, renaming dummy indices from a per-dimension-group pool (d0, d1, …).
#![allow(unused)]
fn main() {
use ocas_atom::tensor::canon::canonicalize_tensors;
use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry};
use ocas_atom::AtomArena;
use ocas_atom::Symbol;
use ocas_core::arena::Arena;

let arena = Arena::new();
let ctx = AtomArena::new(&arena);

let mut reg = TensorRegistry::new();
reg.register(Symbol::new("T"), SymmetrySpec::none());
reg.register(Symbol::new("U"), SymmetrySpec::none());

let i = ctx.var("i");
let j = ctx.var("j");
let k = ctx.var("k");
let t = ctx.fun("T", &[i, j]);
let u = ctx.fun("U", &[j, k]);
let prod = ctx.mul(&[t, u]);

let ct = canonicalize_tensors(&ctx, prod, &reg).unwrap();
println!("canonical: {}", ct.canonical_form); // T(i,d0) * U(d0,k)
}

Symmetry Specification

SpecMeaning
SymmetrySpec::none()All slots are independent
SymmetrySpec::fully_symmetric(n)Slots interchangeable (hidden positions)
SymmetrySpec::fully_antisymmetric(n)Slots antisymmetric (Ricci identity verification)
Custom subsetssymmetric_subsets / antisymmetric_subsets / cyclic fields

Dummy Index Management

dummy::refresh_dummies renames all labels appearing exactly twice to canonical names (d0, d1, …) per dimension group:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::dummy::refresh_dummies;

let result = refresh_dummies(&ctx, expr, &reg)?;
// "T(i,j)*U(j,i)" → "T(d0,d1)*U(d1,d0)"
}

Young Projector (explicit)

young::young_project expands a tensor with a Young tableau symmetry into a sum over permutations, with signs for antisymmetric columns:

#![allow(unused)]
fn main() {
use ocas_atom::tensor::young::{YoungTableau, young_project};

// Fully antisymmetric [1,1]: f(a,b) → f(a,b) - f(b,a)
let result = young_project(&ctx, f_ab, &YoungTableau::new(vec![1, 1]));
// Fully symmetric [2]: g(a,b) → g(a,b) + g(b,a)
let result = young_project(&ctx, g_ab, &YoungTableau::new(vec![2]));
}

Python API

import ocas

# Canonicalise
result = ocas.canonicalize_tensors("T(i,j)*U(j,k)", {"T": "none", "U": "none"})

# Symmetric canonicalisation (g(b,a) == g(a,b))
result = ocas.canonicalize_tensors("g(b,a)", {"g": "symmetric"})

# Young projector
result = ocas.young_project("f(a,b)", [1, 1])

# Refresh dummy indices
result = ocas.refresh_dummies("T(i,j)*U(j,i)", {"T": "none", "U": "none"})

Benchmarks & Comparison

oCAS ships a criterion-based benchmark suite in ocas-tests/benches/, plus cross-language comparison harnesses for SymPy, SageMath, and Symbolica. This chapter explains how to run each and what they measure.


Running oCAS benchmarks

# All benchmarks
cargo bench --workspace

# A specific benchmark
cargo bench --bench poly_gcd
cargo bench --bench poly_factor
cargo bench --bench groebner
cargo bench --bench integrate
cargo bench --bench ntt_poly_mul

# Faster, less precise runs
cargo bench --bench poly_gcd -- --warm-up-time 0.5 --measurement-time 1
BenchmarkCovers
arenaArena allocation throughput
parse / normalizeExpression parsing and normalization
poly_dense / poly_sparsePolynomial arithmetic
poly_gcdUnivariate and bivariate polynomial GCD
poly_factorSquare-free and full factorization (ℤ/𝔽_p/multivariate/ANF)
groebnerGröbner bases (cyclic-n ideals)
calculus / rewriteDifferentiation, Taylor, rule-based simplification
integrateSymbolic integration (rational/Risch/special functions)
eval_interpreter / eval_jit / eval_simdNumeric evaluation paths
eval_streamingStreaming evaluation (constant memory)
eval_estrin / eval_pulpFast polynomial evaluation (Estrin / pulp SIMD)
ntt_poly_mulNTT polynomial multiplication
gmp_integerGMP integer arithmetic
sympy_comparisonHead-to-head vs SymPy

SymPy comparison (automated)

The sympy_comparison benchmark drives SymPy through a uv-managed Python subprocess (scripts/compare_sympy.py) and feeds the elapsed nanoseconds into criterion via iter_custom, so oCAS and SymPy appear side-by-side in the same report.

# Requires `uv` on PATH; the Python env is provisioned automatically.
cargo bench --bench sympy_comparison

Supported tasks: parse, diff, expand, factor, gcd, series.


SageMath comparison (local, manual)

SageMath is too heavy to install in CI, so the comparison runs locally via the sage interpreter. The harness mirrors bench_sympy.py's output contract.

# From the ocas-tests directory (requires SageMath installed)
sage scripts/bench_sage.py factor "x^30 - 1" 100

Tasks: parse, diff, expand, factor, gcd. Note that SageMath uses ^ for exponentiation (same as oCAS), so no syntax translation is needed.


JIT & evaluation

The eval_jit benchmark compares the Cranelift JIT against the stack-based interpreter on single- and multi-output workloads (1000 calls each, criterion iter_custom timing):

WorkloadInterpreterJITSpeedup
Polynomial (single output)221 µs2.27 µs97×
Trig 3-output479 µs22.4 µs21×

The figures above are from the 0.15.0 measurements (criterion iter_custom, 1000 calls each) and may drift slightly with later releases.

cargo bench --bench eval_jit --features jit

The multi-output case compiles three expressions (sin(x), cos(x), sin(x)/cos(x)) into one evaluator via compile_multi, sharing the sin(x) subexpression across outputs; call_into writes results into a stack-allocated buffer so each call performs zero heap allocation.

Streaming evaluation

StreamingEvaluator reuses internal buffers across rows, so processing a million-row dataset uses constant memory regardless of stream length:

WorkloadPer-row evaluateStreamingEvaluatorSpeedup
100k rows, polynomial23.4 ms16.8 ms28%
cargo bench --bench eval_streaming

f32 mixed precision

compile_jit_f32 / compile_vector_evaluator_f32 generate single-precision code. On the same hardware the SIMD evaluator doubles its lane count (16 lanes vs 8 for f64 on AVX-512). Use when f32 accuracy is sufficient.


Symbolica comparison (local, manual)

Symbolica is the primary performance reference for oCAS. Because Symbolica uses an AGPL-style license and ships as a separate Cargo workspace, it is not linked into the oCAS build. Instead, run Symbolica's own example binaries from the source checkout and compare timings manually.

Recommended comparison matrix (Symbolica examples → oCAS benchmarks):

Symbolica exampleoCAS benchmarkWorkload
polynomial_gcdpoly_gcdInteger & rational polynomial GCD
factorizationpoly_factorx^n - 1 square-free / full factorization
groebner_basisgroebnercyclic-4 ideal
derivativecalculusSymbolic differentiation
seriescalculusTaylor expansion
# Run a Symbolica example (from the symbolica source root)
cd ../symbolica
cargo run --release --example polynomial_gcd

# Then run the corresponding oCAS benchmark
cd ../ocas
cargo bench --bench poly_gcd -- --warm-up-time 0.5 --measurement-time 1

Correctness comparison

Beyond performance, oCAS ships a correctness cross-validation framework in ocas-tests/tests/correctness/. It runs 201 automated tests across 19 mathematical modules (as of 0.24.0), comparing oCAS results against SymPy, SageMath, and Symbolica. See the Correctness chapter for details.


Reporting results

criterion writes HTML reports to target/criterion/. Open target/criterion/poly_gcd/index.html in a browser to inspect distributions, regressions, and side-by-side comparisons.

Correctness

oCAS includes an automated correctness cross-validation framework that compares results against three reference systems: SymPy, SageMath, and Symbolica. This chapter describes the framework, how to run it, and its current known limitations.


Framework overview

The correctness suite lives in ocas-tests/tests/correctness/ and contains 201 #[test] tests across 19 mathematical modules (as of 0.24.0; 35 of them are marked #[ignore] to track known gaps). Each test:

  1. Generates an input (expression, polynomial, system of equations, etc.)
  2. Computes a result using oCAS
  3. Computes the equivalent result using a reference system
  4. Asserts that both results are semantically equal

The modules cover the full breadth of oCAS functionality:

ModuleTestsCovers
calculus16Differentiation, Taylor, integration (SymPy-checked)
evaluation6Numeric evaluation
finite_field5Finite field arithmetic
groebner24Gröbner basis computation
integral_risch15Risch symbolic integration
integral_rules1100-case rule-table sample cross-checked against SymPy
integral_verify3Numerical wrong-answer guard (independent oracle; the 0.27.1 C14/D7b and √(p/q) classes, plus the 0.27.3 fold/special-function/half-power mechanisms)
linear_solve5Linear solvers
matrix5Matrix operations
normalize8Expression normalization
ntheory11Number theory (cross-checked against SymPy ntheory)
ode34ODE solving
parse12Expression parsing and printing (incl. unary minus / bare subtraction)
partial_fraction8Partial fraction decomposition
poly_arithmetic6Dense/sparse polynomial arithmetic
poly_factor12Square-free and full factorization
poly_factor_anf21Algebraic number field factorization
poly_gcd5Polynomial GCD
resultant8Resultants
rewrite8Rewriting and simplification
root_isolation4Real root isolation

217 correctness tests in total (plus the crate-level unit tests in ocas-calc, ocas-parse, ocas-atom, ocas-poly, …).


Ignored tests (known gaps)

Some tests are marked #[ignore] (34 in total) to track known gaps — they are expected to fail and are only run manually when reproducing or advancing the issue (cargo test -p ocas-tests --test correctness -- --ignored). The Wilkinson n=10 root-isolation gap is closed: the exact dyadic Sturm evaluation (0.27.x) finds all 10 roots.

Since 0.27.2 the integration suite has a second, independent correctness criterion: integral_verify re-checks integration results with the numerical oracle in ocas-tests/src/integral_eval.rs (5-point central differences at sample points with deterministic dummy parameters), so an antiderivative that looks plausible but is wrong fails the suite. The same oracle drives the verified_solved / verify_mismatches fields of the Rubi 1892-problem coverage harness.


Running the tests

# Run all correctness tests
cargo test -p ocas-tests --test correctness

# Run a specific module (e.g. ODE solving)
cargo test -p ocas-tests --test correctness ode

# Run the ignored tests (known gaps)
cargo test -p ocas-tests --test correctness -- --ignored

# Run with verbose output to inspect failures
cargo test -p ocas-tests --test correctness -- --nocapture

The tests require no external dependencies — all reference computations use SymPy through a uv-managed Python subprocess that provisions itself automatically.


Comparison harnesses

Separate scripts provide manual cross-checking against SageMath and Symbolica for deeper investigation:

# SageMath (requires `sage` installed locally)
cd ocas-tests
sage scripts/bench_sage.py factor "x^30 - 1" 100

# Symbolica (requires Symbolica source checkout)
cd ../symbolica
cargo run --release --example factorization

These harnesses were used to validate the automated test suite during development and are maintained for manual regression testing.


Audit report

ocas-tests/scripts/generate_audit_report.py runs the full suite (including the --ignored tests) and generates an audit report:

cd ocas-tests
python scripts/generate_audit_report.py

The report is written to docs/planning/correctness/audit-<date>.md and includes:

  • Pass/fail/ignored count summaries for the regular and ignored test runs
  • A list of failing tests
  • A factorization-timing comparison against Symbolica

Known limitations

IssueModuleStatus
sin(x)^2 + cos(x)^2 → 1 requires egg featurerewriteSimplifies with the egg feature
Bernoulli forcing y^n confuses linear coefficient extractionodeKnown limitation (#[ignore])
Integrator missing tan/sec table entriesodePlanned (#[ignore])

See also

Numerical Backends

oCAS ships a pure-Rust default build (using num-bigint/num-rational) and optional high-performance backends behind feature flags. The default build uses only LGPL-compatible dependencies; GPL-exclusive code is isolated in the optional ocas-gpl crate.

Feature flags

FeatureCrateBackendLicense
gmpocas-core, ocas-domain, ocas-polyGMP integers/rationals via rugLGPL
mpfrocas-domainMPFR real balls via rugLGPL
flintocas-polyFLINT 3 (Linux/WSL only)LGPL
nttocas-polyNumber-theoretic transform for polynomial multiplication (self-implemented)LGPL
sprsocas-polySparse matrix backend (via sprs crate)MIT
fast-polyocas-evalFast polynomial evaluation (SIMD-friendly)MIT
jitocas-evalCranelift JIT compilationApache-2.0
simdocas-evalSIMD-vectorized evaluationMIT
eggocas-rewriteE-graph equality saturationMIT

Enabling backends

# GMP + MPFR backends (recommended on Windows via MSYS2 MINGW64)
cargo build -p ocas --features gmp,mpfr

# FLINT backend (Linux/WSL only; requires system libflint)
cargo build -p ocas --features gmp,mpfr,flint

# JIT + SIMD evaluation
cargo build -p ocas --features jit,simd

See docs/build-windows.md for Windows-specific setup of GMP/MPFR via MSYS2.

GPL backends

The ocas-gpl crate (licensed GPL-3.0-or-later) is opt-in and never included in the default build. It hosts GPL-exclusive algorithms that cannot ship under LGPL. Default oCAS builds remain LGPL-clean.

Building on Windows

The recommended Windows toolchain is MSYS2 MINGW64 (GNU target), which provides GMP/MPFR for the optional numerical backends. MSVC builds work for the pure-Rust default (no backends) configuration.

MSYS2 MINGW64 setup

# In the MSYS2 MINGW64 shell
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-gmp mingw-w64-x86_64-mpfr

Then build with backends:

cargo build -p ocas --features gmp,mpfr

MSVC (no backends)

cargo build --target x86_64-pc-windows-msvc --no-default-features

The flint feature is not supported on Windows due to upstream flint3-sys limitations (POSIX-only types).

For the full step-by-step guide including PATH configuration and troubleshooting, see docs/build-windows.md in the repository.

Contributing

Contributions are welcome! oCAS follows trunk-based development on main with short-lived feature branches and pull requests.

Development setup

  • Rust 1.97+ (Edition 2024)
  • A C compiler (MSVC, GCC, or Clang)
  • Optional: GMP/MPFR/FLINT system libraries for backend features

Quality gates

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --features egg --exclude ocas-py -- -D warnings
cargo test --workspace --exclude ocas-py --features egg
cargo deny check

Python bindings

cd ocas-py
uv venv .venv
uv pip install pytest maturin
uv run maturin develop
uv run python -m pytest ../ocas-tests/tests/python/ -v

Coding standards

  • Follow the Rust API Guidelines.
  • Run cargo fmt --all before submitting.
  • Prefer arena allocation for expression trees; avoid per-node Box/Rc.
  • Use thiserror for error types; propagate with ?.
  • Backend code lives behind feature flags; GPL code only in ocas-gpl.
  • Public APIs for stable releases must include rustdoc examples.

See the repository's CLAUDE.md and ARCHITECTURE_EN.md for the full conventions, layered-dependency rules, and release process.

External References

  • Rust API documentation (docs.rs) — auto-generated reference for the public Rust API, built from the published crate with portable features only (backend features such as gmp/mpfr/flint that require system C libraries are excluded). For the full backend API docs, build locally with cargo doc -p ocas --features gmp,mpfr,flint --no-deps.
  • crates.io registry page — published releases, feature flags, and dependency graph.
  • GitHub repository — source code, issue tracker, and contribution guide.
  • Documentation site — this book, served from GitHub Pages. Versioned snapshots live under /ocas/v<version>/ for each release tag.