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):
| Version | Date | Key additions |
|---|---|---|
| 0.1.0 | 2026-06-29 | Workspace setup, arena allocator, unified error types, minimal C ABI example, cross-platform CI |
| 0.2.0 | 2026-06-30 | Expression tree Atom (hash consing), parser based on logos/chumsky, normalizer |
| 0.3.0 | 2026-06-30 | Coefficient domains (Integer/Rational/FiniteField/RealBall/Complex), dense/sparse polynomials, optional GMP/FLINT backends |
| 0.4.0 | 2026-07-01 | Rewrite engine (wildcard patterns, AC matching, Rule/simplify), optional egg e-graph equality saturation |
| 0.5.0 | 2026-07-01 | Symbolic differentiation diff, Taylor expansion taylor, heuristic integration integrate |
| 0.6.0 | 2026-07-08 | Stabilized prelude API, rustdoc examples, proptest property tests, Criterion benchmarks, SymPy comparison tooling |
| 0.7.0 | 2026-07-01 | Root isolation (Sturm's theorem), linear system solvers (ℚ/ℤ), Diophantine equations, Buchberger Gröbner bases |
| 0.8.0 | 2026-07-02 | Evaluation engine (stack VM, CSE optimization, SIMD vectorization, Cranelift JIT backend) |
| 0.9.0 | 2026-07-02 | Python bindings (PyO3 Expression/ExpressionEvaluator), C calculus API, C++ RAII wrappers, 33 pytest cases |
| 0.10.0 | 2026-07-02 | Python Polynomial/Matrix classes, Bareiss determinant, FiniteField implements EuclideanDomain, mdBook documentation site |
| 0.11.0 | 2026-07-03 | Correctness comparison framework (16 modules, 82 tests), complete ℤ/𝔽_p factorization (Cantor–Zassenhaus + Hensel), number theory primitives, multivariate GCD |
| 0.12.0 | 2026-07-04 | RationalPolynomial rational function type, Brown PRS resultant, Karatsuba fast multiplication, partial fraction decomposition apart/together |
| 0.13.0 | 2026-07-06 | F4 Gröbner basis algorithm (Gebauer–Moeller filtering, SimpCache, ℤ_p i64 fast path) |
| 0.14.0 | 2026-07-18 | Risch symbolic integration (Hermite reduction, RDE, trigonometric/special functions), FGLM change of order, Hilbert bound |
| 0.15.0 | 2026-07-20 | Multi-output JIT, f32 mixed precision, streaming evaluation (constant memory), arena reset + workspace pool, native i64 F4 pipeline |
| 0.16.0 | 2026-07-21 | Arbitrary multivariate factorization (Wang EEZ), $n$-ary GCD (dense recursive evaluation–interpolation) |
| 0.17.0 | 2026-07-22 | Algebraic number field factorization (Trager norm algorithm), $\mathrm{GF}(p^d)$ construction |
| 0.18.0 | 2026-07-23 | Resource control Fuel, hyper-dual automatic differentiation HyperDual, Vegas Monte Carlo integration, tensor foundations (index slots/contraction/symmetrization) |
| 0.19.0 | 2026-07-23 | F5 signature reduction algorithm (cyclic-6 ℤ₁₃ speedup ~1400× to 2.63s), unified Gröbner dispatch entry point, Rust 1.97 toolchain |
| 0.20.0 | 2026-07-27 | ODE solvers (5 first-order + 2 second-order + power series), ODE classification engine classify_ode |
| 0.20.1 | 2026-07-27 | Integrating factors, variation of parameters (VOP), extended method of undetermined coefficients (resonance/trig/superposition), reduction of order |
| 0.21.0 | 2026-07-30 | BPSW primality testing, integer factorization (Pollard rho/p−1/p+1/ECM), discrete logarithms, modular polynomial GCD (Brown), 12 number theory Python bindings |
| 0.22.0 | 2026-08-02 | McKay graph-isomorphism canonical labeling engine, tensor expression canonicalization, Young projectors, backtracking AC matching, multi-pattern replacement |
| 0.23.0 | 2026-08-02 | Ideal operations (membership/sum/product/quotient/saturation/intersection), elimination orders, zero-dimensional solving, primary decomposition, Hilbert series, rational root theorem |
| 0.24.0 | 2026-08-03 | Heuristic integration (LIATE integration by parts/trig substitution/Weierstrass), DoubleF64 double-precision floats (~31 significant digits) |
| 0.25.0 | 2026-08-04 | Multi-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.0 | 2026-08-04 | SWAR-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.0 | 2026-09-06 | Symbolic-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.1 | 2026-09-10 | Integration-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.2 | 2026-09-11 | Verified-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.3 | 2026-09-12 | 0.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.0 | 2026-09-13 | Integration-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:
| crate | Responsibility |
|---|---|
ocas-core | Arena allocator, Fuel resource control, ThreadPool, GMP type aliases |
ocas-domain | Coefficient domain traits (Domain/EuclideanDomain), Integer/Rational/FiniteField/RealBall/Complex/DoubleF64/AlgebraicExtension, number theory functions, assumption system |
ocas-poly | Dense univariate/sparse multivariate polynomials, RationalPolynomial, matrices, GCD, factorization, Gröbner bases (Buchberger/F4/F5), FGLM, ideal operations |
ocas-atom | Atom/Arena/Symbol expression tree, hash consing, tensors (graph canonicalization/Young projectors) |
ocas-parse | Expression parser based on logos/chumsky |
ocas-rewrite | Wildcard pattern matching (AC backtracking), Rule/simplify/transform, optional egg e-graph |
ocas-calc | Symbolic differentiation diff, layered integration pipeline (rational → Risch → trigonometric → special functions → heuristic), Taylor expansion, ODE solvers (first/second-order, series, Laplace, systems), partial fractions |
ocas-eval | Stack VM interpreter, SIMD vectorized evaluation, Cranelift JIT, streaming evaluation, Vegas Monte Carlo numeric integration |
ocas | Top-level re-export crate + prelude::* |
ocas-py | PyO3 Python bindings (25 classes + 32 functions) |
ocas-c | C/C++ FFI bindings (91 #[no_mangle] exports) + ocas.hpp RAII wrappers |
ocas-gpl | GPL-licensed backend isolation (GMP/MPFR/FLINT) |
ocas-tests | Correctness 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
| Topic | Description | Entry point |
|---|---|---|
| Rust API reference | Per-item documentation of all public types and functions | API reference overview |
| Python API reference | PyO3 binding signatures, parameters, exceptions, examples | Python API |
| C/C++ API reference | FFI prototypes, error codes, memory management, examples | C/C++ API |
| Mathematical foundations | Progressive exposition from polynomial algebra to the Risch algorithm | Math overview |
| Algorithm details | Implementation details of Gröbner bases, factorization, number theory, integration, ODEs | Algorithms |
| Solvers | Linear/Diophantine/polynomial-system/ODE solvers | Solvers |
| Rewriting and simplification | Pattern matching, rule-based simplification, e-graphs | Rewriting |
| Evaluation and JIT | VM interpreter, SIMD, Cranelift JIT | Evaluation |
| Numeric integration | Vegas adaptive Monte Carlo | Numeric integration |
| Automatic differentiation | Forward-mode AD with hyper-dual numbers | Autodiff |
| Tensors | Indices, contraction, canonicalization, Young projectors | Tensors |
| Benchmarks and performance | Criterion benchmarks, SymPy/SageMath comparisons | Performance |
| Correctness | Cross-system validation tests | Correctness |
| Build guide | Backend selection, Windows build, contributing | Build |
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
Language Minimum version Notes Rust 1.97+ (edition 2024) Install via rustup recommended Python 3.10+ Use pip installor build from sourceC/C++ C11 / C++17 Building 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:
| Feature | Description |
|---|---|
gmp | Use GMP arbitrary-precision arithmetic (instead of pure-Rust num-bigint) |
mpfr | Use MPFR ball arithmetic (real interval arithmetic) |
jit | Cranelift JIT-compiled evaluator |
simd | SIMD-vectorized batch evaluation |
egg | e-graph equality saturation engine |
ntt | NTT fast polynomial multiplication (𝔽_p) |
mimalloc | Use the mimalloc allocator |
system-libs | Link 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:
- Expression system — Arena, Symbol, Atom, pattern matching
- Coefficient domains — Integer, Rational, FiniteField, etc.
- Polynomials — univariate/multivariate polynomials, monomial orders
- Matrices — determinant, solving, multiplication
- Calculus — diff, integrate, taylor
- Solvers — linear equations, Diophantine, ODEs
- Number theory — primality, factorization, discrete logarithms
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/:
| File | Description |
|---|---|
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 useopt-level = 3) - For releases use
cargo build --release(enables LTO) - Use
sccacheorcargo-nextestto 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
- Rust API reference — complete documentation of Rust modules, types, and functions
- Python API reference — detailed documentation of all Python classes and functions
- C/C++ API reference — C exported functions, C++ RAII wrappers, memory management
- Architecture — overall design and module relationships of oCAS
- Gröbner basis implementation — Buchberger / F4 / F5 algorithms in detail
- ODE solving — classification and solution methods for ordinary differential equations
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.
| Level | crate | Responsibility |
|---|---|---|
| 5 Bindings | ocas, ocas-py, ocas-c | Rust, Python, C/C++ public API |
| 4 Application | ocas-calc, ocas-eval, ocas-parse | Calculus, evaluation, parsing |
| 3 Symbol engine | ocas-atom, ocas-rewrite | Atom, converters, pattern matching, e-graph |
| 2 Algebra kernel | ocas-domain, ocas-poly | Domains, polynomials, number theory |
| 1 Numerical backend | ocas-core | GMP encapsulation (the FLINT backend lives in ocas-poly) |
| 0 Runtime | ocas-core | Arena, 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
| Crate | Responsibility |
|---|---|
ocas-core | Arena bump allocator, unified error type OcasError, fuel counter Fuel |
ocas-domain | Coefficient-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-atom | Expression tree (Atom, AtomNode, AtomArena), hash-consing, tensor system |
ocas-parse | String → Atom parser |
ocas-poly | Polynomials (dense univariate, sparse multivariate, rational function fields), matrices, Gröbner-basis algorithms (Buchberger/F4/F5), ideal operations, FGLM order conversion |
ocas-rewrite | Pattern matching (AC matching), simplification (fixed-point iteration), bottom-up transformation, E-graph (optional) |
ocas-calc | Symbolic differentiation, integration (Risch pipeline), Taylor expansion, ODE solving, Diophantine/linear/polynomial-system solvers |
ocas-eval | Stack-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
| Name | Kind | Description |
|---|---|---|
Atom | struct | Copy handle pointing to an expression node in the arena |
AtomArena | struct | Hash-consing constructor that builds expressions from an Arena |
AtomNode | enum | Node variants: Num/Var/Fun/Add/Mul/Pow |
Symbol | struct | Interned string identifier, Copy |
normalize | fn | Normalizes an expression (sorting, merging like terms) |
parse | fn | Parses a string into an Atom |
ParseError | enum | Parse error |
Coefficient Domains
| Name | Kind | Description |
|---|---|---|
Domain | trait | Basic coefficient-domain arithmetic (zero/one/add/mul/sub/neg, etc.) |
EuclideanDomain | trait | Euclidean domain (+ div_rem/gcd/lcm) |
Integer | struct | Arbitrary-precision integer |
IntegerDomain | struct | Integer domain implementation |
Rational | struct | Arbitrary-precision rational number |
RationalDomain | struct | Rational number field implementation |
FiniteField | struct | 𝔽ₚ finite field |
FiniteFieldElement | struct | Finite-field element |
RealBall | struct | Real ball arithmetic (requires the mpfr feature) |
RealBallDomain | struct | Real ball domain implementation |
Complex | struct | Complex number |
ComplexDomain | struct | Complex number field implementation |
DoubleF64 | struct | Double-precision float (~31 significant digits) |
DoubleF64Domain | struct | Double-precision float domain implementation |
AlgebraicExtension | struct | Algebraic number field ℚ(α) or GF(p^d) |
AlgebraicElement | struct | Algebraic number field element |
AlgebraicNumberField | struct | Algebraic number field constructor |
Assumption | enum | Assumption variants (Positive/Negative/Integer/Real/Complex, and 8 more) |
Assumptions | struct | Assumption set |
SymbolAssumptions | struct | Per-symbol assumption management |
Polynomials
| Name | Kind | Description |
|---|---|---|
DenseUnivariatePolynomial | struct | Dense univariate polynomial |
SparseMultivariatePolynomial | struct | Sparse multivariate polynomial |
RationalPolynomial | struct | Element p/q of a rational function field |
RootInterval | struct | An interval containing exactly one real root (low/high bounds) |
MonomialOrder | trait | Monomial order trait |
Lex | struct | Lexicographic order |
Grlex | struct | Graded lexicographic order |
Grevlex | struct | Graded reverse lexicographic order |
WeightOrder | struct | Weighted order |
BlockOrder | struct | Block elimination order |
SubOrder | struct | Sub-order |
monomial_divides | fn | Monomial divisibility test |
monomial_lcm | fn | Monomial least common multiple |
monomial_are_coprime | fn | Monomial coprimality test |
Matrices
| Name | Kind | Description |
|---|---|---|
Matrix | struct | Matrix over a domain (Bareiss determinant, Gaussian-elimination solving) |
MatrixError | enum | Matrix operation error |
Gröbner Bases
| Name | Kind | Description |
|---|---|---|
GroebnerBasis | struct | Gröbner-basis result (polynomial list plus metadata) |
buchberger | fn | Buchberger algorithm entry point |
f4 | fn | F4 algorithm entry point (matrix row-echelon batching) |
Calculus
| Name | Kind | Description |
|---|---|---|
diff | fn | Symbolic differentiation |
integrate | fn | Symbolic integration (layered pipeline) |
integrate_outcome | fn | Certified integration: three-valued outcome + symbolic certificate (0.28.0) |
integrate_outcome_with_options | fn | Certified integration with options (0.28.0) |
integrate_heuristic | fn | Heuristic integration (without Risch) |
integrate_with_fuel | fn | Integration with a fuel limit |
taylor | fn | Taylor expansion |
substitute | fn | Expression substitution |
apart | fn | Partial-fraction decomposition |
Solvers
| Name | Kind | Description |
|---|---|---|
solve_linear_rational | fn | Linear system Ax=b over ℚ |
solve_linear_integer | fn | Linear system Ax=b over ℤ |
solve_diophantine | fn | Diophantine equation ax+by=c |
DiophantineSolution | struct | Diophantine solution (particular solution + steps) |
SolveError | enum | Solve error |
solve_polynomial_system | fn | Polynomial system solving (zero-dimensional / positive-dimensional / empty) |
classify_ode | fn | ODE type classification |
dsolve | fn | Symbolic ODE solving |
dsolve_ivp | fn | ODE initial-value problem (Laplace) |
dsolve_system | fn | 2×2 ODE system solving |
ODE | struct | ODE description |
ODESolution | enum | ODE solution (explicit/implicit/parametric/series/system/unsolved) |
ODEType | enum | ODE type enumeration |
Rewriting & Simplification
| Name | Kind | Description |
|---|---|---|
Pattern | enum | Match pattern (Literal/Wildcard/Add/Mul/Pow/Fun) |
Rule | struct | Rewrite rule |
Bindings | struct | Pattern binding results |
MatchError | enum | Match error (incl. BudgetExhausted) |
WildcardLevel | enum | Wildcard level (Single/Sequence/NullSequence) |
match_pattern | fn | AC pattern matching |
simplify | fn | Fixed-point simplification |
simplify_with_fuel | fn | Simplification with a fuel limit |
transform | fn | Bottom-up transformation |
Evaluation & JIT
| Name | Kind | Description |
|---|---|---|
ExpressionEvaluator | struct | Stack-VM interpreter |
FunctionMap | struct | Custom function registry |
EvaluationDomain | trait | Evaluation-domain constraint |
EvaluationError | enum | Evaluation error |
EvalTree | struct | Evaluation tree |
Instr | enum | Instruction enum |
Instruction | struct | Instruction details |
Slot | enum | Operand slot (Param/Const/Temp) |
PowfExtension | trait | Floating-point power extension |
VectorEvaluator | struct | SIMD batch evaluation (requires the simd feature) |
JitEngine | struct | JIT compilation engine (requires the jit feature) |
JitCompiledFunction | struct | JIT compilation result |
Numeric Integration
| Name | Kind | Description |
|---|---|---|
Vegas | struct | Vegas Monte Carlo integrator |
VegasOptions | struct | Vegas options (bin count, sample count, iterations, etc.) |
IntegrateResult | struct | Integration result (value + error) |
Integrator | struct | Integrator interface |
StatisticsAccumulator | struct | Inverse-variance weighted accumulator |
integrate_1d | fn | 1-D numeric integration convenience function |
Automatic Differentiation
| Name | Kind | Description |
|---|---|---|
DualShape | struct | Derivative layout description |
HyperDual | struct | Hyper-dual number |
DualCoeff | trait | Dual coefficient constraint |
new_first_order | fn | First-order dual convenience constructor |
Tensors
| Name | Kind | Description |
|---|---|---|
Tensor | struct | Named-index tensor |
IndexSlot | struct | Index slot (label + position) |
IndexPosition | enum | Upper (contravariant) / Lower (covariant) |
Symmetry | enum | None/Symmetric/Antisymmetric |
Contracted | enum | Contraction result (Scalar/Product) |
TensorProduct | struct | Tensor product |
contract | fn | Index contraction |
symmetrise_sign | fn | Antisymmetrization sign |
Runtime
| Name | Kind | Description |
|---|---|---|
Arena | struct | Bump allocator |
OcasError | enum | Unified error type |
Result | type | Result<T, OcasError> |
Fuel | struct | Fuel 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"] }
| Feature | Default | Description | Sub-crate features involved |
|---|---|---|---|
gmp | no | Use GMP arbitrary-precision arithmetic (significantly speeds up big-integer operations) | ocas-domain/gmp, ocas-poly/gmp, ocas-core/gmp |
mpfr | no | Enable MPFR real ball arithmetic (the RealBall type) | ocas-domain/mpfr |
flint | no | Use the FLINT number-theory library to accelerate polynomial operations | ocas-poly/flint |
python | no | Enable Python bindings (PyO3) | dep:ocas-py |
gpl | no | Enable GPL-licensed feature modules | dep:ocas-gpl |
egg | no | Enable E-graph simplification (the egg_simplify function) | ocas-rewrite/egg |
jit | no | Enable Cranelift JIT compilation (the compile_jit method) | ocas-eval/jit |
simd | no | Enable SIMD batch evaluation (VectorEvaluator) | ocas-eval/simd |
mimalloc | no | Use the mimalloc global allocator (accelerates many-small-allocation workloads) | dep:mimalloc |
system-libs | no | Link against preinstalled system GMP/MPFR/MPC (required on MinGW) | ocas-domain/system-libs |
ntt | no | NTT number-theoretic transform acceleration for polynomial multiplication over 𝔽ₚ | ocas-poly/ntt |
sprs | no | Sparse matrix backend (used for the F4 prep phase) | ocas-poly/sprs |
fast-poly | no | fast_polynomial Estrin evaluation acceleration | ocas-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
| Topic | File | Contents |
|---|---|---|
| Expression system | rust-expressions | Arena, Atom, AtomArena, Symbol, pattern matching, parsing |
| Coefficient domains | rust-domains | Domain trait, integers/rationals/finite fields/real balls/complex/doubles/algebraic number fields, assumption system |
| Polynomials | rust-polynomials | Dense univariate, sparse multivariate, rational function fields, monomial orders |
| Matrices | rust-matrix | Matrix (Bareiss determinant, Gaussian elimination) |
| Calculus | rust-calculus | diff, integrate, taylor, integration pipeline layers |
| Solvers | rust-solvers | Linear systems, Diophantine equations, polynomial systems, ODE solving |
| Rewriting & simplification | rust-rewrite | Pattern, Rule, simplify, transform, E-graph |
| Evaluation & JIT | rust-evaluation | ExpressionEvaluator, JIT, SIMD, FunctionMap |
| Automatic differentiation | rust-autodiff | DualShape, HyperDual, DualCoeff |
| Tensors | rust-tensors | Tensor, contraction, symmetry, canonicalization, Young projection |
| Number theory | rust-ntheory | Primality, factorization, discrete logarithms, CRT, number-theoretic functions, quadratic residues |
| Gröbner bases & ideals | rust-groebner | Buchberger/F4/F5, FGLM, ideal operations, primary decomposition |
| Factoring | rust-factoring | Univariate/multivariate ℤ[x]/𝔽ₚ[x] factoring, algebraic number fields, rational functions |
| Numeric integration | rust-numeric-integration.md | Vegas 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
- Architecture Overview — overall design and data flow
- Mathematics Overview — background mathematics and learning paths
- Python API Reference — Python bindings
- C/C++ API Reference — C/C++ bindings
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:
| Parameter | Type | Description |
|---|---|---|
block_size | usize | Size 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:
| Parameter | Type | Description |
|---|---|---|
init | impl FnOnce() -> T | Closure 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
name | &str | The 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.
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)→42Var("x")→xFun("sin", [x])→sin(x)Add([x, y])→x + y(subexpressions are parenthesized unless they are leaf nodes)Mul([x, y])→x*yPow(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
| Variant | Description |
|---|---|
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$ becomesMul([x, Pow(y, Num(-1))]). There are no standalone subtraction or division nodes in the AST. - The argument lists of
AddandMulare sorted afternormalize, 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"), } }
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:
| Parameter | Type | Description |
|---|---|---|
arena | &'a Arena | Reference 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:
| Parameter | Type | Description |
|---|---|---|
value | i64 | The 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:
| Parameter | Type | Description |
|---|---|---|
name | &str | The 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:
| Parameter | Type | Description |
|---|---|---|
name | &str | The 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
base | Atom<'a> | The base. |
exp | Atom<'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:
| Parameter | Type | Description |
|---|---|---|
atoms | &[Atom<'a>] | Slice of Atoms to allocate. |
Return value: &'a [Atom<'a>].
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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The arena context used to allocate nodes. |
input | &str | The expression string to parse. |
Return value: Result<Atom<'a>, ParseError>.
Errors:
| Variant | Description |
|---|---|
ParseError::Lex(LexError) | The input contains an illegal character. |
ParseError::UnexpectedEof | The input ends unexpectedly (e.g., "x +"). |
ParseError::UnexpectedToken | An 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
AddandMul(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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The arena context used to allocate the result. |
atom | Atom<'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"); }
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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The arena context. |
expr | Atom<'a> | The expression to substitute into. |
var | Symbol | The variable name to replace. |
replacement | Atom<'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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The arena context. |
atom | Atom<'a> | The expression to transform. |
f | FnMut(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:
| Parameter | Type | Description |
|---|---|---|
atom | Atom<'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:
| Parameter | Type | Description |
|---|---|---|
atom | Atom | The 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.
| Variant | Description |
|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
_ctx | &'a impl PatternAlloc<'a> | The pattern allocator (usually &()). |
atom | Atom<'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.
| Variant | Naming convention | Description |
|---|---|---|
Single | One trailing _ in the name (e.g., x_; a leading _ also works, e.g. _x) | Matches exactly one Atom. |
Sequence | Two 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. |
NullSequence | Three 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:
| Parameter | Type | Description |
|---|---|---|
pattern | Pattern<'a> | The matching pattern. |
atom | Atom<'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:
| Parameter | Type | Description |
|---|---|---|
pattern | Pattern<'a> | The matching pattern. |
atom | Atom<'a> | The expression to match against. |
max_backtracks | usize | The 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:
| Parameter | Type | Description |
|---|---|---|
name | Symbol | The 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.
| Variant | Description |
|---|---|
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.
| Variant | Display | Description |
|---|---|---|
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
- Rewrite & Simplification — the full rewrite engine:
Rule,simplify,replace_all, and more - Calculus —
diff,integrate,taylor, and other operations built on the expression system - Coefficient Domains — the
Domaintrait and the various domain types
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 type | Bounds | Description |
|---|---|---|
Element | Clone + PartialEq + Eq + Debug + 'static | The type of elements in the domain |
Methods:
| Method | Signature | Description |
|---|---|---|
zero | fn zero(&self) -> Self::Element | Additive identity |
one | fn one(&self) -> Self::Element | Multiplicative identity |
add | fn add(&self, a, b) -> Self::Element | Addition |
sub | fn sub(&self, a, b) -> Self::Element | Subtraction |
neg | fn neg(&self, a) -> Self::Element | Negation |
mul | fn mul(&self, a, b) -> Self::Element | Multiplication |
div | fn div(&self, a, b) -> Option<Self::Element> | Division. Returns None if b is zero or the division is not exact |
inv | fn inv(&self, a) -> Option<Self::Element> | Multiplicative inverse. Returns None if a is zero |
is_zero | fn is_zero(&self, a) -> bool | Whether a is the additive identity (default: *a == self.zero()) |
is_one | fn is_one(&self, a) -> bool | Whether a is the multiplicative identity (default: *a == self.one()) |
mul_assign | fn 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_assign | fn sub_mul_assign(&self, a: &mut E, b: &E, c: &E) | Fused subtract-multiply *a -= b * c. Used heavily in F4 row echelonization |
pow | fn pow(&self, a, n: u64) -> Self::Element | Non-negative integer power. Defaults to binary exponentiation; finite fields override with modpow |
cast_u64 | fn cast_u64(&self, n: u64) -> Self::Element | Converts 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:
| Parameter | Type | Description |
|---|---|---|
a | &Self::Element | Dividend |
b | &Self::Element | Divisor |
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:
divrequires exact division (zero remainder), otherwise returnsNoneinvreturns a result only for $\pm 1$, otherwiseNonepowuses 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:
| Parameter | Type | Description |
|---|---|---|
value | T: 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:
| Parameter | Type | Description |
|---|---|---|
exp | u32 | Non-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:
| Parameter | Type | Description |
|---|---|---|
exp | &Integer | Exponent (non-negative) |
modulus | &Integer | Modulus (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, & × &):
| Trait | Operation |
|---|---|
Add | + |
Sub | - |
Mul | * |
Div | / (integer division, truncated toward zero) |
Rem | % |
Neg | unary - |
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:
divreturns an exact result for every non-zero divisorinvreturns a result for every non-zero elementEuclideanDomain'sdiv_remdegenerates 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:
| Parameter | Type | Description |
|---|---|---|
numer | i64 | Numerator |
denom | i64 | Denominator (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:
| Trait | Operation |
|---|---|
Add | + |
Sub | - |
Mul | * |
Div | / |
Neg | unary - |
AddAssign / SubAssign / MulAssign / DivAssign | compound assignment |
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:
| Parameter | Type | Description |
|---|---|---|
prime | BigInt | The 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:
| Parameter | Type | Description |
|---|---|---|
value | impl 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
divinverts via Fermat's little theorem: $a^{-1} \equiv a^{p-2} \pmod{p}$invas above; returnsNonefor the zero elementpowuses themodpowoptimization (much faster than the default binary exponentiation)
EuclideanDomain implementation
div_rem: division is exact in a field; the remainder is always zerogcd: 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:
| Parameter | Type | Description |
|---|---|---|
mid | f64 (or rug::Float) | Midpoint |
rad | f64 (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 addmul: 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 intervaldiv: $a / b = a \cdot b^{-1}$, returnsNonewhen the ball contains zeroinv: $1/(mid \pm rad)$, returnsNonewhen 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:
| Parameter | Type | Description |
|---|---|---|
base | D: Domain | The 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}$, returnsNonewhen the denominator is zeroinv: implemented viadiv(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
| Constant | Value | Description |
|---|---|---|
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
| Method | Signature | Description |
|---|---|---|
abs | pub fn abs(self) -> Self | Absolute value |
is_nan | pub fn is_nan(self) -> bool | Whether the value is NaN |
is_infinite | pub fn is_infinite(self) -> bool | Whether the value is infinite |
is_finite | pub fn is_finite(self) -> bool | Whether 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
| Method | Signature | Description |
|---|---|---|
sqrt | pub fn sqrt(self) -> Self | Square root by Newton iteration. Returns NaN for negative input |
exp | pub fn exp(self) -> Self | Exponential function, Taylor series + argument reduction (by $\ln 2$) |
ln | pub fn ln(self) -> Self | Natural logarithm, Newton iteration. Returns NaN for non-positive input |
sin | pub fn sin(self) -> Self | Sine, Taylor series + reduction to $[-\pi, \pi]$ |
cos | pub fn cos(self) -> Self | Cosine, $\cos(x) = \sin(\pi/2 - x)$ |
tan | pub fn tan(self) -> Self | Tangent, $\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.
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:
| Parameter | Type | Description |
|---|---|---|
base | D: Domain | The base domain |
min_poly | Vec<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 zerogcd: 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
| Variant | Meaning | Display |
|---|---|---|
Real | real number | "real" |
Complex | complex number | "complex" |
Integer | integer | "integer" |
Rational | rational number | "rational" |
Positive | strictly positive ($> 0$) | "positive" |
Negative | strictly negative ($< 0$) | "negative" |
NonNegative | non-negative ($\geq 0$) | "non-negative" |
NonPositive | non-positive ($\leq 0$) | "non-positive" |
NonZero | non-zero | "non-zero" |
Finite | finite (not $\pm\infty$) | "finite" |
Even | even | "even" |
Odd | odd | "odd" |
Prime | prime | "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:
| Assumption | Implies |
|---|---|
Positive | NonNegative, NonZero, Real |
Negative | NonPositive, NonZero, Real |
NonNegative | Real |
NonPositive | Real |
Integer | Rational, Real |
Rational | Real |
Complex | Real |
Even | Integer |
Odd | Integer |
Prime | Integer, 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:
| Assumption | Conflicts with |
|---|---|
Positive | Negative, NonPositive |
Negative | Positive, NonNegative |
NonNegative | Negative |
NonPositive | Positive |
Even | Odd |
Odd | Even |
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→AssumptionsAssumptions | Assumption→AssumptionsAssumptions | 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 polynomialsSparseMultivariatePolynomial<D, O>— sparse multivariate polynomialsRationalPolynomial<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
- DenseUnivariatePolynomial
- SparseMultivariatePolynomial
- RationalPolynomial
- Helper Functions
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:
| Parameter | Type | Description |
|---|---|---|
self | &Self | The 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:
- The monomial with the larger total degree $\sum \alpha_i$ comes first
- 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:
- The monomial with the larger total degree comes first
- 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:
| Parameter | Type | Description |
|---|---|---|
weights | SmallVec<[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:
| Parameter | Type | Description |
|---|---|---|
boundaries | SmallVec<[usize; 4]> | List of split points for the ordering (exclusive upper bounds, not including n_vars) |
orders | SmallVec<[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:
| Parameter | Type | Description |
|---|---|---|
matrix | Vec<Vec<i64>> | $n \times n$ weight matrix (row-major) |
elim_vars | usize | Number of leading variables to eliminate |
n_vars | usize | Total 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:
| Parameter | Type | Description |
|---|---|---|
domain | D | The 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:
| Parameter | Type | Description |
|---|---|---|
domain | D | The coefficient domain |
coeffs | Vec<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:
| Parameter | Type | Description |
|---|---|---|
other | &Self | The 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:
| Parameter | Type | Description |
|---|---|---|
n | u32 | Non-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:
| Parameter | Type | Description |
|---|---|---|
polys | &mut [Self] | List of pairwise coprime polynomials |
b | &Self | The 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); }
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:
| Parameter | Type | Description |
|---|---|---|
domain | D | The coefficient domain |
n_vars | usize | Number 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:
| Parameter | Type | Description |
|---|---|---|
domain | D | The coefficient domain |
n_vars | usize | Number of variables |
terms | Vec<(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:
| Parameter | Type | Description |
|---|---|---|
var_index | usize | Variable 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:
| Parameter | Type | Description |
|---|---|---|
var_index | usize | Index of the variable to substitute |
value | &D::Element | The 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:
| Parameter | Type | Description |
|---|---|---|
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:
- Compute $\gcd(\text{num}, \text{den})$ and cancel it
- In the univariate case, reduce by an exact GCD via the dense Euclidean algorithm
- 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Variant | Output |
|---|---|
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 positioni * ncols + jindata Dis constrained toEuclideanDomain, supportingdiv,gcd, etc., which makes fraction-free elimination possible
Trait implementations:
Index<(usize, usize)>/IndexMut<(usize, usize)>— element access viamatrix[(i, j)]Display— one line per row, elements separated by spacesDebug,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:
| Parameter | Type | Description |
|---|---|---|
nrows | usize | Number of rows |
ncols | usize | Number of columns |
data | Vec<D::Element> | Elements in row-major order; length must equal nrows * ncols |
domain | D | The 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:
| Parameter | Type | Description |
|---|---|---|
nrows | usize | Number of rows |
ncols | usize | Number of columns |
domain | D | The 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)); }
identity
Signature: pub fn identity(n: usize, domain: D) -> Self
Function: Creates the n × n identity matrix.
Parameters:
| Parameter | Type | Description |
|---|---|---|
n | usize | Matrix dimension |
domain | D | The 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:
| Parameter | Type | Description |
|---|---|---|
rows | Vec<Vec<D::Element>> | Each inner Vec is one row; all rows must have the same length |
domain | D | The 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)); }
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:
| Parameter | Type | Description |
|---|---|---|
i | usize | Row 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)]); }
column
Signature: pub fn column(&self, j: usize) -> Vec<D::Element>
Function: Returns a copy of the j-th column.
Parameters:
| Parameter | Type | Description |
|---|---|---|
j | usize | Column 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)]); }
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:
| Parameter | Type | Description |
|---|---|---|
i | usize | Index of the first row |
j | usize | Index of the second row |
start_col | usize | Start 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 traceErr(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:
| Parameter | Type | Description |
|---|---|---|
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 determinantErr(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)); }
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 squareErr(MatrixError::Inconsistent)— the matrix is singular (no solution)Err(MatrixError::Underdetermined { .. })— singularity leads to an underdetermined systemErr(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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
b | &[D::Element] | The right-hand side vector; length must equal self.nrows |
Return value:
Ok(Vec<D::Element>)— the solution vector of lengthself.ncolsErr(MatrixError::ShapeMismatch)—b.len() != self.nrowsErr(MatrixError::Inconsistent)— the system has no solutionErr(MatrixError::Underdetermined { rank })— infinitely many solutions,rank < ncolsErr(MatrixError::ResultNotInDomain)— the solution is not in the domain (e.g., integer division fails)
Algorithm steps:
- Build the augmented matrix $[A \mid b]$
- Run fraction-free row echelon reduction (
row_echelon) on the firstncolscolumns - Check whether the constant terms of the rows below the rank are zero (consistency check)
- Run fraction-free back substitution (
back_substitution) - If the rank < number of variables, return
Underdetermined - 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 { .. }))); }
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:
| Parameter | Type | Description |
|---|---|---|
max_col | usize | The 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
- Expression System — expression tree construction and manipulation
- Coefficient Domains — the
EuclideanDomaintrait and domain implementations - Polynomials — polynomial types using the same domain traits
- Linear Algebra Foundations — the mathematics of the Bareiss algorithm and Gaussian elimination
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:
| Submodule | Purpose |
|---|---|
derivative | symbolic differentiation diff |
integral | layered integration pipeline integrate, integrate_heuristic, integrate_with_fuel |
series | Taylor expansion taylor, substitution substitute |
partial_fraction | partial fraction decomposition apart, recomposition together |
tower | differential field tower construction for the Risch algorithm |
ode | ordinary 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | the expression arena used to construct the result |
expr | Atom<'a> | the expression to differentiate |
var | Symbol | the 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 type | Differentiation 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) }
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_outcomewhen you need an antiderivative with a machine-checkable symbolic certificate.
Parameters:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | the expression arena |
expr | Atom<'a> | the integrand |
var | Symbol | the 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 functionc * xVar(var): $\int x,dx = \frac{1}{2} x^2$; other variables are treated as constantsAdd: integrate each term recursively and sumMul(integrate_product): recognizes the formc · f(x)(a constant times a non-constant factor), extracts the constant factor, and integrates recursivelyPow(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):
-
Rational function integration (
integrate_rational): when the expression is a rational function of $\var$ (with no other variables or function applications), perform:- Integrate the polynomial part term by term
- Hermite reduction to separate the rational part
- Logarithmic part: completing the square (quadratic denominators → $\log$ or $\text{atan}$) + Rothstein–Trager resultant
-
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. -
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, andrealifyconverts the answer back to real form. -
Special functions (
special_integrate): when both Risch and the trigonometric rewriting fail, tries to match known special-function antiderivatives. See special function integration. -
Heuristics (
heuristic_integrate): integration by parts (LIATE), trigonometric substitution, Weierstrass substitution, Euler substitution. See integrate_heuristic. -
Unevaluated form (
fallback): when everything above fails, returnsIntegral(expr, var). Detectable viais_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]:
| Value | Meaning |
|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | the expression arena |
expr | Atom<'a> | the integrand |
var | Symbol | the 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):
| Priority | Type | LIATE score | Example |
|---|---|---|---|
| 0 (highest) | Logarithmic (L) | 0 | $\log(x)$ |
| 1 | Inverse trigonometric (I) | 1 | $\text{asin}(x)$ |
| 2 | Algebraic (A) | 2 | $x^2$ |
| 3 | Trigonometric (T) | 3 | $\sin(x)$ |
| 4 | Exponential (E) | 4 | $\exp(x)$ |
| 5 (lowest) | Other | 5 | — |
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:
| Integrand | Substitution | Result |
|---|---|---|
| $\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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | the expression arena |
expr | Atom<'a> | the integrand |
var | Symbol | the integration variable |
fuel | &Fuel | the fuel budget (rewrite step limit) |
Returns:
Ok(Atom<'a>)— integration simplification succeededErr(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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | the expression arena |
expr | Atom<'a> | the expression to expand |
var | Symbol | the expansion variable |
point | Atom<'a> | the expansion point (any expression, usually 0 or a constant) |
order | usize | the 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:
- For $n = 0, 1, \dots, \text{order}$:
- Compute $f^{(n)}(x)$ (via
diff) - Evaluate at $x = \text{point}$ with
substituteto obtain $f^{(n)}(\text{point})$ - Multiply by $\frac{1}{n!}$ (via
mul_by_factorial_inverse) - Multiply by $(x - \text{point})^n$
- Compute $f^{(n)}(x)$ (via
- 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | the expression arena |
expr | Atom<'a> | the expression to rewrite |
var | Symbol | the variable name to replace |
replacement | Atom<'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 matchvar - No automatic simplification after substitution (call
simplifyseparately 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)"); }
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
poly_part | Option<&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 degree | Strategy |
|---|---|
| 0 | constant integration |
| 1 | $c \cdot \log(ax + b)$ |
| 2 | complete the square → $\log$ or $\text{atan}$ |
| ≥ 3 | Rothstein–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 resultNone— 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 }
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
logandexpare accepted (trigonometric functions must first be rewritten viatrig_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:
- Hermite reduction (
hermite_tower): decompose $\frac{a}{d}$ into $D(g) + \frac{a_1}{d_1}$, where $d_1$ is square-free - 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
- 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 pattern | Real 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 succeededNone— 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
| Integrand | Antiderivative | Function 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).
- polynomial $\times F(a + b x)$ for
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 functionsNone— no known pattern matches; the caller returnsIntegral(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.
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
integratepasses through two simplification stages: first the default rewrite rulesdefault_rules(algebraic identities such as $0 + x = x$, $1 \cdot x = x$, $x^1 = x$) for 20 steps, thencalculus_rulesfor 10 steps, and finallynormalizeto canonical form diffandtaylorsimplify withcalculus_rulesdirectly, thennormalize
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
| Limit | Value | Purpose |
|---|---|---|
MAX_DEPTH | 8 | integrate_raw recursion depth |
MAX_RISCH_DEPTH | 16 | Risch algorithm recursion depth |
PARTS_MAX_DEPTH | 2 | integration-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
- Math: Symbolic calculus — the mathematics of differentiation and Taylor expansion
- Math: The Risch algorithm — full mathematical derivation of the differential field tower and the Risch algorithm
- Math: Polynomial GCD and factorization — the mathematical foundations of Hermite reduction and square-free factorization
- Solvers API — ODE solving (
dsolve,classify_ode) - Expression system — basic usage of
Atom,AtomArena,Symbol
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:
| Parameter | Type | Description |
|---|---|---|
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$ guaranteedErr(SolveError): see the error list below
Errors:
SolveError::EmptySystem— $A$ or $\mathbf{b}$ is emptySolveError::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 domainSolveError::Matrix(_)— an internal matrix operation failed (converted viaFrom<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:
| Parameter | Type | Description |
|---|---|---|
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 vectorErr(SolveError): the error types are the same as forsolve_linear_rational, with a possible additionalResultNotInDomain(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:
| Field | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
a | i64 | The coefficient of $x$ |
b | i64 | The coefficient of $y$ |
c | i64 | The constant on the right-hand side |
Returns: Option<DiophantineSolution>
Some(sol): the particular/general solution structure when a solution existsNone: $\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:
| Field | Type | Description |
|---|---|---|
values | Vec<f64> | The variable values $[x_1, x_2, \dots, x_n]$, in the order of the variables in the original system |
multiplicity | usize | The 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:
| Field | Type | Description |
|---|---|---|
solutions | Vec<RealSolution> | All real solutions (may contain approximations) |
vector_space_dimension | usize | The 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:
| Variant | Description |
|---|---|
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 |
Empty | The 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:
| Parameter | Type | Description |
|---|---|---|
equations | &[SparseMultivariatePolynomial<RationalDomain, Lex>] | The polynomial system; each polynomial represents one equation $f_i = 0$ |
algo | Algorithm | Grö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:
- 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 - If the GB is {1}, return
Empty(the system is inconsistent, no solutions) - Determine whether it is zero-dimensional: whether each variable $x_i$ has a pure-power leading monomial $x_i^{N_i}$ in the GB
- 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
- 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:
| Field | Type | Description |
|---|---|---|
equation | Atom<'a> | The normalized form of the equation (lhs - rhs) |
func | Atom<'a> | The unknown function, e.g., y(x) |
var | Symbol | The 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:
| Variant | Description |
|---|---|
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:
| Variant | Equation form | Description |
|---|---|---|
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 |
PowerSeries | linear 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
ODETypevariants.
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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The expression arena |
ode | ODE<'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)); }
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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The expression arena |
ode | ODE<'a> | The ODE to solve |
hint | Option<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):
- Call
classify_odeto obtain the list of candidate methods - Try each method in priority order
- The solution of the first successful method is returned
ODESolution::Unsolvedis 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The expression arena |
ode | ODE<'a> | The ODE (must be linear with constant coefficients) |
y0 | Atom<'a> | The initial condition $y(0)$ |
y1 | Option<Atom<'a>> | $y'(0)$ for second-order problems; ignored for first-order problems |
Returns: ODESolution<'a>
Explicit(expr): an explicit solution without free constantsUnsolved: the Laplace transform method is not applicable or the inverse cannot be computed
Method description:
- 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)$
- Substitute the initial conditions to obtain an algebraic equation for $Y(s)$
- Solve for $Y(s)$
- 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:
| Parameter | Type | Description |
|---|---|---|
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)] |
var | Symbol | The 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:
| Variant | Description |
|---|---|
EmptySystem | The input system of equations is empty |
NonLinear | The system is nonlinear in the target variables |
NonSquare | The coefficient matrix is not square (equation count ≠ unknown count) |
Inconsistent | The 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 |
ResultNotInDomain | The 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The expression arena |
equation | Atom<'a> | The ODE equation (in lhs - rhs form) |
func | Atom<'a> | The unknown function (e.g., y(x)) |
sol | Atom<'a> | The candidate solution expression |
var | Symbol | The independent variable symbol |
Returns: Atom<'a> — the residual expression after substitution and simplification. If zero, the candidate solution is correct.
Source modules
| Module | Path | Contents |
|---|---|---|
| Linear solving | ocas-calc/src/solve.rs | solve_linear_rational, solve_linear_integer, solve_diophantine, SolveError |
| Polynomial systems | ocas-poly/src/ideal.rs | solve_polynomial_system, PolynomialSystemSolution, RealSolution, ZeroDimSolutions |
| ODE classification | ocas-calc/src/ode/classify.rs | classify_ode, ODEType |
| First-order ODEs | ocas-calc/src/ode/first_order.rs | separable, linear, Bernoulli, exact, and homogeneous solvers |
| Second-order ODEs | ocas-calc/src/ode/second_order.rs | constant-coefficient, Cauchy-Euler, reduction-of-order, and variation-of-parameters solvers |
| Series solutions for ODEs | ocas-calc/src/ode/series.rs | power series and Frobenius solvers |
| Laplace transforms for ODEs | ocas-calc/src/ode/laplace.rs | the Laplace transform implementation behind dsolve_ivp |
| ODE systems | ocas-calc/src/ode/systems.rs | the 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
Ruleabstraction pairing a pattern with a replacement closure - Simplification engine:
simplify/simplify_with_fuelapply rules iteratively until a fixed point - Replacement:
replace_once/replace_all/replace_all_multiplecontrolled replacement - Transformation:
transformbottom-up traversal,partition_exprcombinatorial partitioning - E-graphs: equality saturation simplification based on the
eggcrate (requires theeggfeature)
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.
| Variant | Description | Naming convention |
|---|---|---|
Single | Matches exactly one atom | Variable name ends with _, e.g. x_ |
Sequence | Matches one or more atoms (non-empty sequence) | Variable name starts with __, e.g. __x |
NullSequence | Matches 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.
| Variant | Description |
|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
ctx | &impl PatternAlloc<'a> | The pattern allocator (pass &()) |
atom | Atom<'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.
| Variant | Description |
|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
name | Symbol | The 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.
| Variant | Description |
|---|---|
NoMatch | The pattern structure does not match |
InconsistentBinding | The same-named wildcard is bound to different values |
BudgetExhausted | AC 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:
| Parameter | Type | Description |
|---|---|---|
pattern | Pattern<'a> | The pattern to match |
atom | Atom<'a> | The target expression |
Return value:
Ok(Bindings)— match succeeded, containing all wildcard bindingsErr(MatchError)— match failed
Matching semantics:
Add/Mulnodes use AC (associative-commutative) matching: argument order does not matter, associativity is handled automaticallyFunnodes 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:
| Parameter | Type | Description |
|---|---|---|
pattern | Pattern<'a> | The pattern to match |
atom | Atom<'a> | The target expression |
max_backtracks | usize | The 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:
| Parameter | Type | Description |
|---|---|---|
pattern | Pattern<'a> | The matching pattern |
replacement | Fn(&Bindings, &AtomArena) -> Atom | The 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:
| Parameter | Type | Description |
|---|---|---|
condition | Fn(&Bindings) -> bool | The 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The expression arena context |
atom | Atom<'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:
| Rule | Rewrite | Description |
|---|---|---|
add_zero | x + 0 → x | Additive identity |
add_zero_left | 0 + x → x | Additive identity (left) |
mul_zero | x × 0 → 0 | Multiplicative zero |
mul_zero_left | 0 × x → 0 | Multiplicative zero (left) |
mul_one | x × 1 → x | Multiplicative identity |
mul_one_left | 1 × x → x | Multiplicative identity (left) |
add_same | x + x → 2x | Combine like terms |
pow_zero | x⁰ → 1 | Zero-th power |
pow_one | x¹ → x | First power |
Parameters:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The expression arena context |
atom | Atom<'a> | The expression to simplify |
rules | &[Rule<'a>] | The list of rewrite rules |
iter_limit | usize | The maximum number of iterations (10–20 recommended) |
Return value: Atom<'a> — the simplified expression (registered in the arena).
Behavior:
- Each iteration traverses the expression tree bottom-up
- All rules are tried in turn on each node
- If any rule fires, the round produced a change and the next round runs
- Stops when no rule fires or when
iter_limitis 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The expression arena context |
atom | Atom<'a> | The expression to simplify |
rules | &[Rule<'a>] | The list of rewrite rules |
iter_limit | usize | The maximum number of iterations |
fuel | &Fuel | The 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The expression arena context |
atom | Atom<'a> | The target expression |
pattern | Pattern<'a> | The matching pattern |
replacement | Fn(&Bindings, &AtomArena) -> Atom | The 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The expression arena context |
atom | Atom<'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:
| Field | Type | Description |
|---|---|---|
pattern | Pattern<'a> | The matching pattern |
replacement | F | The replacement closure |
condition | Option<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.
| Field | Type | Default | Description |
|---|---|---|---|
once | bool | false | Whether to replace only the first match |
bottom_up | bool | false | Whether to traverse bottom-up (default is top-down) |
nested | bool | false | Whether 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The expression arena context |
atom | Atom<'a> | The target expression |
f | FnMut(Atom) -> Option<Atom> | The transformation closure |
Return value: Atom<'a> — the transformed expression.
Behavior:
- Recursively traverses the subexpressions (leaf nodes are returned directly)
- Rebuilds the current node with the transformed subexpressions
- Calls
fon the rebuilt node - If
freturnsSome, the new node is used; ifNone, 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &AtomArena<'a> | The expression arena context |
expr | Atom<'a> | The input expression (must be of the form arg(...)) |
bins | &[(Symbol, usize)] | The bin list, each element (function name, capacity) |
fill_last | bool | Whether surplus elements are absorbed into the last bin |
repeat | bool | Whether 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:
| Parameter | Type | Description |
|---|---|---|
elements | &[T] | The elements to distribute |
bins | &[(B, usize)] | The bin list (name, capacity) |
fill_last | bool | Whether surplus elements are absorbed into the last bin |
repeat | bool | Whether 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.
| Field | Type | Description |
|---|---|---|
coefficient | usize | The multinomial coefficient |
bins | Vec<(B, Vec<T>)> | The name and contents of each bin |
E-Graph Simplification
Feature Gate: requires the
eggfeature. Add toCargo.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:
| Parameter | Type | Description |
|---|---|---|
atom | Atom<'a> | The expression to simplify |
ocas_arena | &AtomArena<'a> | The oCAS expression arena |
iter_limit | usize | The equality saturation iteration limit |
Return value: Atom<'a> — the simplified expression.
Built-in E-graph rules:
| Rule name | Rewrite |
|---|---|
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:
| Flag | Purpose |
|---|---|
jit | enables the Cranelift JIT compilation backend |
simd | enables SIMD batch evaluation (pulp, runtime detection of SSE2/AVX2/AVX-512) |
fast-poly | enables 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:
| Variant | Description |
|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
atom | Atom<'_> | 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:
| Pattern | Rule |
|---|---|
Add | remove zero terms; sum fully constant terms; fold single terms |
Mul | absorb zero factors; remove unit factors; multiply fully constant factors; fold single factors |
Pow | x^1 → x, x^0 → 1, evaluate Num^Num directly |
Fun | evaluate 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:
| Parameter | Type | Description |
|---|---|---|
atom | Atom<'_> | 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:
| Parameter | Type | Description |
|---|---|---|
atom | Atom<'_> | the source expression |
map | FunctionMap<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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
params | &[T] | the parameter values; length must equal param_count() |
Returns: Ok(Vec<T>) with length equal to result_count().
Errors:
| Error | Condition |
|---|---|
WrongArity | params.len() != param_count() |
DivisionByZero | division by zero encountered during evaluation |
FunctionNotFound | an 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Error | Condition |
|---|---|
UnsupportedOperation | contains instructions the JIT cannot lower (e.g., external functions, sec/csc/cot) |
JitCompilationError | the 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
instructions | &[Instr] | the instruction sequence |
param_count | usize | the 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
rows | I: IntoIterator<Item = S> | the input row iterator; each row is AsRef<[T]> of length param_count() |
sink | FnMut(&[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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
name | &str | the function name (case-insensitive) |
arity | usize | the expected number of arguments |
func | Box<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.
| Variant | Description |
|---|---|
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.
| Variant | Format | Description |
|---|---|---|
Add | dst = sum(sources) | summation |
Mul | dst = product(sources) | product |
Pow | dst = base^exp | integer power |
Powf | dst = base^exp | floating-point power |
Fun | dst = builtin(src) | built-in function |
ExternalFun | dst = fns[idx](srcs...) | external function |
Assign | dst = src | copy |
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)]
| Variant | Description |
|---|---|
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.
| Variant | Math | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
name | &str | the 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:
| Method | Signature | Description |
|---|---|---|
from_f64 | fn from_f64(f64) -> Self | create a value from an f64 |
zero | fn zero() -> Self | additive identity |
one | fn one() -> Self | multiplicative identity |
add_ref | fn add_ref(&self, &Self) -> Self | addition |
sub_ref | fn sub_ref(&self, &Self) -> Self | subtraction |
mul_ref | fn mul_ref(&self, &Self) -> Self | multiplication |
div_ref | fn div_ref(&self, &Self) -> Result<Self> | division; division by zero returns DivisionByZero |
neg_ref | fn neg_ref(&self) -> Self | negation |
powi_ref | fn powi_ref(&self, i64) -> Self | integer power; returns 1 when exp == 0 |
resolve_builtin | fn resolve_builtin(name: &str, &Self) -> Result<Self> | execute a built-in function |
Implementations:
| Type | Description |
|---|---|
f64 | standard double precision (always available) |
DoubleF64 | oCAS 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.
| Variant | Description |
|---|---|
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) |
DivisionByZero | division 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:
| Type | Description |
|---|---|
Rational | arbitrary-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:
| Parameter | Type | Description |
|---|---|---|
components | Vec<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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
nvars | usize | the 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:
| Parameter | Type | Description |
|---|---|---|
shape | &Arc<DualShape> | the component layout |
i | usize | the variable index (0-based), must be < n_vars() (out-of-range indices are silently ignored, no panic) |
c | T | the 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:
| Parameter | Type | Description |
|---|---|---|
shape | &Arc<DualShape> | the component layout |
c | T | the 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:
| Parameter | Type | Description |
|---|---|---|
i | usize | the 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:
| Parameter | Type | Description |
|---|---|---|
shape | Arc<DualShape> | the component layout |
values | Vec<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:
| Trait | Operation | Derivative rule |
|---|---|---|
Add | a + b | $\frac{\partial}{\partial x_i}(a+b) = a'_i + b'_i$ |
Sub | a - b | $\frac{\partial}{\partial x_i}(a-b) = a'_i - b'_i$ |
Neg | -a | $\frac{\partial}{\partial x_i}(-a) = -a'_i$ |
Mul | a * b | $\frac{\partial}{\partial x_i}(ab) = a'b + ab'$ (higher-order terms handled via the multiplication table) |
Div | a / b | equivalent 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
| Limitation | Description |
|---|---|
| Rational coefficients only | DualCoeff is currently implemented only for Rational; f64 and other floating-point types are not supported |
| No transcendental functions | transcendental functions such as sin, cos, exp, ln are not implemented — they require real-coefficient traits |
| Division-by-zero panic | Div calls inv(), which panics when the value component is zero |
| Shape-mismatch panic | Add/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 layout | the default new_first_order tracks only first-order partials; second and higher orders require building the layout manually via DualShape::new |
See also
- Coefficient domains —
Rationaland other domain types - Evaluation and JIT — numerical evaluation (floating point)
- Calculus — symbolic differentiation
diff, integrationintegrate
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:
| Variant | Meaning |
|---|---|
Upper | Contravariant index (superscript); corresponds to a contravariant vector in physics |
Lower | Covariant 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:
| Parameter | Type | Description |
|---|---|---|
label | Atom<'a> | Index label expression (usually a variable name such as mu or i) |
position | IndexPosition | Index 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:
| Variant | Meaning |
|---|---|
None | No symmetry (generic tensor) |
Symmetric | Symmetric: swapping any two index slots leaves the tensor unchanged |
Antisymmetric | Antisymmetric: 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:
| Parameter | Type | Description |
|---|---|---|
name | Symbol | Tensor name (interned string, e.g. "g", "Riemann") |
slots | Vec<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:
| Parameter | Type | Description |
|---|---|---|
symmetry | Symmetry | The 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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::Productcontaining the two original tensors - With a partial contraction, returns
Contracted::Productcontaining 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:
| Variant | Type | Description |
|---|---|---|
Product | TensorProduct<'a> | Partial or no contraction; a tensor product with free indices |
Scalar | Atom<'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:
| Field | Type | Description |
|---|---|---|
factors | Vec<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::NoneorSymmetry::Symmetric: returns+1Symmetry::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:
| Parameter | Type | Description |
|---|---|---|
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:
| Field | Type | Description |
|---|---|---|
symmetric_subsets | Vec<Vec<usize>> | List of symmetric slot subsets. Slots within the same subset are interchangeable |
antisymmetric_subsets | Vec<Vec<usize>> | List of antisymmetric slot subsets. Swapping two slots within a subset flips the sign |
cyclic | Option<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:
| Parameter | Type | Description |
|---|---|---|
rank | usize | The 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:
| Parameter | Type | Description |
|---|---|---|
rank | usize | The 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:
| Parameter | Type | Description |
|---|---|---|
pos | usize | The 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:
| Parameter | Type | Description |
|---|---|---|
name | Symbol | The tensor function head name |
spec | SymmetrySpec | The 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:
| Parameter | Type | Description |
|---|---|---|
label | Symbol | The index label (e.g. "mu", "i") |
group | u64 | The 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:
| Parameter | Type | Description |
|---|---|---|
name | Symbol | The 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:
| Parameter | Type | Description |
|---|---|---|
label | Symbol | The 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The expression arena |
expr | Atom<'a> | The tensor expression to canonicalize (Mul of Fun nodes) |
registry | &TensorRegistry | The tensor registry (symmetry specifications + index groups) |
Returns: Result<CanonicalTensor<'a>, TensorCanonError>
Errors:
| Error variant | Description |
|---|---|
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 |
InconsistentOpenIndices | The free indices of the terms in a sum are inconsistent |
UnsupportedPower | An 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, ®).unwrap(); let ct2 = canonicalize_tensors(&ctx, t_ji, ®).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:
| Field | Type | Description |
|---|---|---|
canonical_form | Atom<'a> | The canonicalized expression (dummy indices renamed) |
external_indices | Vec<Atom<'a>> | The list of free (external) indices |
dummy_indices | Vec<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, ®).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:
| Variant | Description |
|---|---|
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 |
InconsistentOpenIndices | The sets of free indices of the terms in a sum are inconsistent |
UnsupportedPower | An 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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The expression arena |
expr | Atom<'a> | The tensor expression to process |
registry | &TensorRegistry | The tensor registry (used for dimension-group assignment) |
Returns: Result<Atom<'a>, DummyError>
Errors:
| Error variant | Description |
|---|---|
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, ®).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:
| Variant | Description |
|---|---|
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:
| Field | Type | Description |
|---|---|---|
row_lengths | Vec<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:
| Parameter | Type | Description |
|---|---|---|
row_lengths | Vec<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:
| Parameter | Type | Description |
|---|---|---|
ctx | &'a AtomArena<'a> | The expression arena |
tensor_expr | Atom<'a> | The tensor expression (a Fun node) |
tableau | &YoungTableau | The 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:
- Canonicalization (
canonicalize_tensors) — symmetry enforcement at the level of graph isomorphism - 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, ®).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
- Expression System — the
Atom,AtomArena,Symbolbasic types - Rewriting and Simplification — pattern-matching-based expression transformation
- Linear Algebra — matrix operations (a low-rank special case of tensors)
- Tensor Algebra and Canonicalization — the mathematical foundations
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:
| Submodule | Purpose |
|---|---|
primes | Primality testing, prime generation, modular inverse, extended Euclid, quadratic residues |
factor | Complete integer factorization (trial division, Pollard rho/p−1/p+1, ECM) |
crt | Multi-modulus Chinese remainder theorem |
dlog | Discrete logarithms (BSGS, Pohlig–Hellman) |
functions | Multiplicative 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The integer under test |
Return value: bool — true if n passes the BPSW test.
Implementation details:
- First a base-2 strong Miller–Rabin test (shares the
mr_witnesscore withis_prime). - 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$.
- Write $n + 1 = d \cdot 2^r$ ($d$ odd) and compute the Lucas sequences $(U_k, V_k)$ with a binary ladder.
npasses 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:
| Parameter | Type | Description |
|---|---|---|
n | u64 | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
a | &Integer | The element to invert |
m | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
a | &Integer | The first integer |
b | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
a | &Integer | The integer to reduce |
m | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
r1 | &Integer | The first remainder |
m1 | &Integer | The first modulus |
r2 | &Integer | The second remainder |
m2 | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
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
Nonefor an empty list. - Returns
Nonewhen 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:
| Parameter | Type | Description |
|---|---|---|
a | &Integer | The integer under test |
p | &Integer | An odd prime (primality is guaranteed by the caller) |
Return value:
| Value | Meaning |
|---|---|
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 }
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:
| Parameter | Type | Description |
|---|---|---|
a | &Integer | The integer under test |
n | &Integer | A 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:
| Parameter | Type | Description |
|---|---|---|
a | &Integer | The radicand |
p | &Integer | An 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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:
- Trial division (
factor_trial): removes small factors $\leq 1000$. - 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$)
- 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The integer to factor |
rng | &mut Xoshiro256PlusPlus | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The integer to factor |
limit | u64 | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | An odd composite number |
rng | &mut Xoshiro256PlusPlus | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | An odd composite number |
b1 | u64 | The smoothness bound |
rng | &mut Xoshiro256PlusPlus | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | An odd composite number |
b1 | u64 | The smoothness bound |
rng | &mut Xoshiro256PlusPlus | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | An odd composite number |
b1 | u64 | The smoothness bound |
max_curves | u32 | The maximum number of curves to try |
rng | &mut Xoshiro256PlusPlus | The 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:
| Parameter | Type | Description |
|---|---|---|
base | &Integer | The base |
target | &Integer | The target value |
modulus | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
base | &Integer | The base |
target | &Integer | The target value |
p | &Integer | The 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:
pis composite.baseis not a unit modulop.targetis not in the subgroup generated bybase.
Implementation details:
- Factor the order of
basewithfactor_integer. - For each prime power $q^e$, recover the discrete logarithm digit by digit with BSGS (digit recovery).
- Combine the partial results with
crt_many. - 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The input integer |
k | u32 | The 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:
| Parameter | Type | Description |
|---|---|---|
n | &Integer | The 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— algorithm selection enumGroebnerBasis— Gröbner basis structgroebner_basis— unified entry pointbuchberger— Buchberger algorithmf4::f4— F4 matrix algorithmf5::f5— F5 signature algorithmfglm— FGLM order-change algorithmHilbertSeries— Hilbert serieshilbert_series— computing the Hilbert serieseliminate— elimination- Ideal operations —
ideal_contains,ideal_sum,ideal_product, etc. PrimaryComponent— primary componentPolynomialSystemSolution— polynomial system solution result
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:
| Variant | Description |
|---|---|
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. |
F4 | Forces the F4 matrix algorithm (Faugère 1999). Batches S-polynomial reduction into sparse matrix row operations, significantly faster than Buchberger for larger ideals. |
F5 | Forces 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). |
Buchberger | Forces 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:
| Parameter | Constraint | Description |
|---|---|---|
D | Domain | Coefficient domain (e.g., RationalDomain, FiniteField) |
O | MonomialOrder | Monomial order (e.g., Lex, Grevlex) |
Fields:
| Field | Type | Description |
|---|---|---|
basis | Vec<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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Description |
|---|---|
O2 | Target 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:
| Parameter | Type | Description |
|---|---|---|
ideal | &[SparseMultivariatePolynomial<D, O>] | List of generators of the ideal |
algo | Algorithm | Algorithm 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:
| Parameter | Type | Description |
|---|---|---|
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 recomputationDivisorIndex: 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:
FpPolyuses i64 modular arithmetic, converting to/from BigInt only when reading inputs and writing outputs
Parameters:
| Parameter | Type | Description |
|---|---|---|
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_posis the index of the input generator andmonomialis the monomial multiple applied. Signatures are compared under the pot (position-over-term) order: module position first (smaller preferred), then by the monomial orderO. - 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:
| Parameter | Type | Description |
|---|---|---|
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:
- Compute the staircase — the set of monomials not divisible by any leading monomial
- Traverse the staircase monomials in the target order
- For each monomial, compute its normal form under the current basis
- If the normal form is linearly dependent on the vectors seen so far, construct a new basis element from the coefficient relation
- Otherwise, add it to the set of seen vectors
Parameters:
| Parameter | Type | Description |
|---|---|---|
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:
| Field | Type | Description |
|---|---|---|
numerator | Vec<i64> | Coefficients of the numerator $N(t)$ (starting from the constant term) |
denominator_power | usize | Power 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:
| Parameter | Type | Description |
|---|---|---|
degree | usize | The 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
ideal | &[SparseMultivariatePolynomial<D, Lex>] | The generators of the ideal (must be in Lex order) |
elim_vars | usize | The number of variables to eliminate (eliminates $x_0, \dots, x_{\text{elim_vars}-1}$) |
algo | Algorithm | Grö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:
| Parameter | Type | Description |
|---|---|---|
generators | &[SparseMultivariatePolynomial<D, Lex>] | The generators of the ideal |
f | &SparseMultivariatePolynomial<D, Lex> | The polynomial to test |
algo | Algorithm | Grö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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
- Compute a Gröbner basis
- Check whether it is $\langle 1 \rangle$ (empty set)
- Check whether it is zero-dimensional
- Convert to a triangular decomposition in Lex order
- 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:
| Parameter | Type | Description |
|---|---|---|
equations | &[SparseMultivariatePolynomial<RationalDomain, Lex>] | The system of equations (polynomials = 0) |
algo | Algorithm | Grö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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
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:
| Field | Type | Description |
|---|---|---|
primary | Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> | The generators of the primary ideal |
prime | Vec<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:
| Variant | Description |
|---|---|
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 |
Empty | No 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:
| Field | Type | Description |
|---|---|---|
solutions | Vec<RealSolution> | The list of real solutions found |
vector_space_dimension | usize | The 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:
| Field | Type | Description |
|---|---|---|
values | Vec<f64> | The value of each variable (one per variable) |
multiplicity | usize | The algebraic multiplicity of the solution |
Module path quick reference
| Function/Type | Full path |
|---|---|
Algorithm | ocas_poly::Algorithm |
GroebnerBasis | ocas_poly::GroebnerBasis |
groebner_basis | ocas_poly::groebner_basis |
buchberger | ocas_poly::buchberger |
eliminate | ocas_poly::eliminate |
f4::f4 | ocas_poly::groebner::f4::f4 |
f5::f5 | ocas_poly::groebner::f5::f5 |
fglm | ocas_poly::groebner::fglm::fglm |
HilbertSeries | ocas_poly::groebner::hilbert::HilbertSeries |
hilbert_series | ocas_poly::groebner::hilbert::hilbert_series |
ideal_contains | ocas_poly::ideal::ideal_contains |
ideal_sum | ocas_poly::ideal::ideal_sum |
ideal_product | ocas_poly::ideal::ideal_product |
ideal_quotient | ocas_poly::ideal::ideal_quotient |
ideal_saturate | ocas_poly::ideal::ideal_saturate |
ideal_intersection | ocas_poly::ideal::ideal_intersection |
ideal_radical | ocas_poly::ideal::ideal_radical |
primary_decomposition | ocas_poly::ideal::primary_decomposition |
is_zero_dimensional | ocas_poly::ideal::is_zero_dimensional |
solve_polynomial_system | ocas_poly::ideal::solve_polynomial_system |
is_prime_ideal | ocas_poly::ideal::is_prime_ideal |
is_primary_ideal | ocas_poly::ideal::is_primary_ideal |
PrimaryComponent | ocas_poly::ideal::PrimaryComponent |
PolynomialSystemSolution | ocas_poly::ideal::PolynomialSystemSolution |
ZeroDimSolutions | ocas_poly::ideal::ZeroDimSolutions |
RealSolution | ocas_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 path | Description |
|---|---|
ocas_poly::factor | Top-level factorization entry point |
ocas_poly::factor::hensel | Hensel lifting and Zassenhaus recombination ($\mathbb{Z}[x]$) |
ocas_poly::factor::finite_field | $\mathbb{F}_p[x]$ factorization (Cantor–Zassenhaus + Berlekamp) |
ocas_poly::factor::multivariate | Bivariate Hensel lifting (Wang's algorithm) |
ocas_poly::factor::eez | Multivariate EEZ Hensel lifting (Wang + leading-coefficient reconstruction) |
ocas_poly::factor::algebraic | Algebraic number field factorization (Trager's algorithm) |
ocas_poly::gcd | GCD (pseudo-remainder + Euclidean algorithm) |
ocas_poly::gcd::modular | Modular GCD (Brown 1971) |
ocas_poly::resultant | Resultant (Brown PRS) |
ocas_poly::rational | Rational function fraction field |
ocas_calc::partial_fraction | Partial 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:
- The input must be primitive (content = 1; preprocess with
primitive_part()if needed). - Square-free factorization (Yun's algorithm): use $\gcd(f, f')$ to separate multiplicities.
- Call
factor_square_freeon each square-free component (internally it handles non-monic inputs via the leading-coefficient transformation, then callsfactor_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.
- 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:
| Parameter | Type | Description |
|---|---|---|
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:
- Square-free factorization (Musser/Bernardin algorithm, handling $p$-th roots in characteristic $p$).
- 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.
- 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:
| Parameter | Type | Description |
|---|---|---|
f | &FpPoly | A 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:
| Parameter | Type | Description |
|---|---|---|
f | &FpPoly | A 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:
| Parameter | Type | Description |
|---|---|---|
base | &FpPoly | The base polynomial |
exp | &BigInt | The exponent (non-negative) |
modulus | &FpPoly | The 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$):
- Extract the content in the main variable and compute the square-free factorization.
- 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.
- Factor the univariate image over $\mathbb{Z}$.
- Wang leading-coefficient reconstruction: distribute the irreducible factors of the total leading coefficient among the univariate factors.
- EEZ variable-by-variable Hensel lifting: recover $x_1, x_2, \dots$ in turn, solving a multivariate Diophantine equation at each step.
- p-adic coefficient Hensel lifting: lift from modulus $p$ to a sufficiently large $p^k$.
- 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:
| Parameter | Type | Description |
|---|---|---|
f | &SparseMultivariatePolynomial<IntegerDomain, Lex> | The polynomial to factor |
x_var | usize | Index of the main variable |
y_var | usize | Index of the secondary variable |
Returns: Vec<(ZMPoly, usize)> — irreducible factor and multiplicity pairs.
Algorithm:
- Bivariate square-free factorization (heuristic bivariate GCD).
- Choose $y = \alpha$ so that the univariate image $f(x, \alpha)$ is square-free with the fewest factors.
- 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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
f | &SparseMultivariatePolynomial<FiniteField, Lex> | The polynomial to factor |
x_var | usize | Index of the main variable |
y_var | usize | Index 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:
| Parameter | Type | Description |
|---|---|---|
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):
- Square-free factorization (Yun's algorithm, using the modular GCD
gcd_anfto avoid coefficient blow-up). - Call
factor_square_free_anfon 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$.
- 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:
| Parameter | Type | Description |
|---|---|---|
field | &AlgebraicNumberField | The 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:
| Parameter | Type | Description |
|---|---|---|
field | &AlgebraicNumberField | The 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:
| Parameter | Type | Description |
|---|---|---|
other | &Self | The 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:
| Parameter | Type | Description |
|---|---|---|
a | &DenseUnivariatePolynomial<IntegerDomain> | The first polynomial |
b | &DenseUnivariatePolynomial<IntegerDomain> | The second polynomial |
Returns: a primitive GCD polynomial.
Algorithm details:
- Choose primes $p > 2^{30}$ (to avoid small-prime issues).
- Compute the monic GCD in $\mathbb{F}_p[x]$, scaled by $\gamma = \gcd(\mathrm{lc}(a), \mathrm{lc}(b))$.
- Combine via CRT (symmetric representatives), discarding "unlucky primes" (where the GCD degree is higher than the true value).
- 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:
| Parameter | Type | Description |
|---|---|---|
other | &Self | The 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
| Method | Signature | Description |
|---|---|---|
is_zero | &self -> bool | Whether the numerator is zero |
is_one | &self -> bool | Whether it is 1/1 |
n_vars | &self -> usize | Number of variables |
domain | &self -> &D | Coefficient domain reference |
Arithmetic Operations
| Method | Signature | Description |
|---|---|---|
add | (&self, &Self) -> Self | Addition (denominator-GCD strategy to reduce intermediate swell) |
sub | (&self, &Self) -> Self | Subtraction |
mul | (&self, &Self) -> Self | Multiplication (cross-cancellation) |
div | (&self, &Self) -> Option<Self> | Division (returns None when the divisor's numerator is zero) |
neg | &self -> Self | Negation $-\frac{n}{d}$ |
inv | &self -> Option<Self> | Inverse (returns None when the numerator is zero) |
pow | (&self, k: u32) -> Self | Power $\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:
| Parameter | Type | Description |
|---|---|---|
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:
| Parameter | Type | Description |
|---|---|---|
poly_part | Option<&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:
| Parameter | Type | Description |
|---|---|---|
other | &Self | The 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:
| Function | Description |
|---|---|
eez_lift | Generic EEZ lifting (over a field, monic) |
eez_lift_imposed | Non-monic EEZ lifting (Wang leading-coefficient imposition) |
eez_lift_z | Integer EEZ lifting (solve over $\mathbb{Q}$ + integrality check) |
coefficient_hensel_lift_z | p-adic coefficient Hensel lifting |
diophantine | Recursive multivariate Diophantine equation solver |
sparse_diophantine_fp | Sparse Diophantine solver with skeleton interpolation |
wang_reconstruct_lcoeffs | Wang leading-coefficient reconstruction |
zassenhaus_multivariate | Multivariate Zassenhaus subset recombination |
Finite Field Factorization
File: ocas-poly/src/factor/finite_field.rs
| Function | Description |
|---|---|
factor_over_finite_field | Top-level entry: square-free + DDF + EDF |
distinct_degree_factorization | DDF: group by the degree of the irreducible factors |
equal_degree_factorization | EDF: randomly split the product of equal-degree factors |
berlekamp | Berlekamp's algorithm (small primes) |
cantor_zassenhaus | DDF + EDF combination |
square_free_factorization_ff | Characteristic-$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
| Function | Description |
|---|---|
factor_anf | Top-level entry: square-free + Trager |
factor_square_free_anf | Trager: norm → $\mathbb{Q}$ factorization → GCD recovery |
norm_with_shift | Trager shift to find a square-free norm |
norm_eval_interp | Norm computation by evaluation–interpolation |
gcd_anf | Modular GCD over $\mathbb{Q}(\alpha)$ |
square_free_anf | Yun square-free factorization (modular GCD) |
factor_square_free_rationals | Square-free factorization over $\mathbb{Q}$ (clear denominators → $\mathbb{Z}$ Hensel) |
See also
- Polynomial GCD and Factorization — mathematical theory and algorithm details
- Algebraic Number Fields and Galois Theory — mathematical background of Trager's algorithm
- Polynomial API — complete API of
DenseUnivariatePolynomialandSparseMultivariatePolynomial - Coefficient Domain API — definitions of
Integer,FiniteField,AlgebraicNumberField
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:
| Field | Type | Description |
|---|---|---|
integral | f64 | The best estimate of the integral |
error | f64 | The 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:
| Parameter | Type | Description |
|---|---|---|
f | &F where F: Fn(&[f64]) -> f64 | The 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:
| Field | Type | Default | Description |
|---|---|---|---|
n_bins | usize | 64 | The number of bins per dimension (the overall grid is the direct product of per-dimension 1-D grids) |
n_samples | usize | 10_000 | The number of samples per iteration |
iterations | usize | 10 | The number of adaptive iterations |
learning_rate | f64 | 1.5 | Grid smoothing/learning rate (typical range 1.0–2.0; larger values make grid updates gentler) |
seed | u64 | 0x0C45 | Random seed (deterministic across runs) |
Implementation details:
- The
Defaultimplementation 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.0is the full step; larger values make grid changes smoother and more stable. - Uses the
Xoshiro256PlusPluspseudo-random number generator (initialized viaseed) 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:
| Parameter | Type | Description |
|---|---|---|
n_dims | usize | The number of integration dimensions (≥ 1) |
opts | VegasOptions | Tuning 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:
| Parameter | Type | Description |
|---|---|---|
f | &F where F: Fn(&[f64]) -> f64 | The 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):
- 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). - Evaluation: compute $f(\mathbf{x})$ and accumulate it into the
StatisticsAccumulatorweighted by the total Jacobian. - Training signal: accumulate $f^2 \cdot w$ into the training slot of the corresponding bin.
- Statistics finalization: call
finalize_iteration()to fold this iteration's estimate into the cross-iteration inverse-variance weighted average. - 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:
| Parameter | Type | Description |
|---|---|---|
f | F where F: Fn(f64) -> f64 | The integrand; receives the physical coordinate $x \in [a, b]$ directly |
a | f64 | The lower limit of integration |
b | f64 | The upper limit of integration (may be less than a, in which case the result is negative) |
opts | VegasOptions | Vegas 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):
| Field | Type | Description |
|---|---|---|
sum_w | f64 | Current iteration: $\sum \text{weight}$ |
sum_wf | f64 | Current iteration: $\sum \text{weight} \cdot f$ |
sum_wf2 | f64 | Current iteration: $\sum \text{weight} \cdot f^2$ |
integral | f64 | The cross-iteration weighted average integral estimate |
error | f64 | The standard error of the integral estimate |
chi_square | f64 | The cross-iteration $\chi^2$ statistic |
iterations | usize | The 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:
| Parameter | Type | Description |
|---|---|---|
weight | f64 | The sample weight (in Vegas, the Jacobian = the product of n_bins × bin_width over all dimensions) |
f | f64 | The 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:
| Parameter | Type | Description |
|---|---|---|
input | str | Symbolic expression string; ^ denotes exponentiation and * denotes multiplication |
Returns: an Expression instance.
Exceptions: ValueError — the expression string failed to parse.
Supported operations:
| Operation | Method | Description |
|---|---|---|
| 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:
| Parameter | Type | Description |
|---|---|---|
var | str | The 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:
| Parameter | Type | Description |
|---|---|---|
var | str | The 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:
| Parameter | Type | Description |
|---|---|---|
var | str | The integration variable |
rules | bool | Whether the rule-table engine is enabled (default True) |
Returns: a dict with the keys:
| Key | Description |
|---|---|
kind | "found" / "proved_nonelementary" / "unknown" |
value | The antiderivative, or the unevaluated Integral(expr, var) form when unknown |
certificate | The certificate layer ("structural" / "field" / "radical"), None when uncertified |
uncertified | The 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:
| Parameter | Type | Description |
|---|---|---|
var | str | The 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:
| Parameter | Type | Description |
|---|---|---|
var | str | The expansion variable |
point | Expression | The expansion center |
order | int | Expansion 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:
| Parameter | Type | Description |
|---|---|---|
var | str | The variable to be replaced |
replacement | Expression | The 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:
| Parameter | Type | Description |
|---|---|---|
coeffs | list | Coefficient list, ordered from lowest to highest degree |
domain | str, FiniteField, or None | Coefficient 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
| Operation | Syntax | Description |
|---|---|---|
| Addition | p + q | Polynomial addition |
| Subtraction | p - q | Polynomial subtraction |
| Multiplication | p * q | Polynomial multiplication |
| Negation | -p | Negates the coefficients |
| Equality | p == q | Comparison of normalized coefficients |
PolynomialFactor
A single factor in a factorization result.
Attributes:
| Attribute | Type | Description |
|---|---|---|
factor | Polynomial | The factor polynomial (primitive over ℤ, monic over GF(p)) |
multiplicity | int | The 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:
| Parameter | Type | Description |
|---|---|---|
terms | dict[tuple[int,...], int] | Keys are exponent tuples (of length n_vars), values are coefficients |
n_vars | int | The 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:
| Parameter | Type | Description |
|---|---|---|
rows | list[list] | Row-major two-dimensional list |
domain | str, FiniteField, or None | Coefficient 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:
| Parameter | Type | Description |
|---|---|---|
rhs | list | The 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
| Operation | Syntax | Description |
|---|---|---|
| Matrix multiplication | a @ b | __matmul__ |
| Addition | a + b | Element-wise addition |
| Subtraction | a - b | Element-wise subtraction |
| Equality | a == b | Element-wise comparison |
Coefficient Domains
IntegerDomain
Selector class for the integer domain ℤ.
Signature:
ocas.IntegerDomain()
Note: the
domainparameter ofPolynomialandMatrixcurrently accepts strings ("integer"/"rational") or aFiniteFieldinstance;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:
| Parameter | Type | Description |
|---|---|---|
modulus | int | Prime modulus, $p \geq 2$ |
Returns: a FiniteField instance.
Exceptions: ValueError — modulus < 2.
Attributes:
| Attribute | Type | Description |
|---|---|---|
modulus | str | Decimal 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:
| Parameter | Type | Description |
|---|---|---|
a | list[list[int]] | Coefficient matrix (row-major, integers) |
b | list[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:
| Parameter | Type | Description |
|---|---|---|
a | list[list[int]] | Coefficient matrix (row-major, integers) |
b | list[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:
| Parameter | Type | Description |
|---|---|---|
a | int | Coefficient of $x$ |
b | int | Coefficient of $y$ |
c | int | Right-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:
| Attribute | Type | Description |
|---|---|---|
particular | tuple[int, int] | A particular solution $(x_0, y_0)$ |
general | tuple[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:
| Parameter | Type | Description |
|---|---|---|
equation | Expression | The ODE expression (in the form equal to zero), e.g. Derivative(y(x), x) - y(x) |
func | str | Name of the unknown function, e.g. "y" |
var | str | Name 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:
| Parameter | Type | Description |
|---|---|---|
equation | Expression | The ODE expression (in the form equal to zero) |
func | str | Name of the unknown function |
var | str | Name of the independent variable |
hint | str or None | A 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:
| Parameter | Type | Description |
|---|---|---|
equation | Expression | The ODE expression (in the form equal to zero) |
func | str | Name of the unknown function |
var | str | Name of the independent variable |
y0 | str | The value of $y(0)$ (string expression, e.g. "1") |
y1 | str or None | The 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:
| Parameter | Type | Description |
|---|---|---|
n | int | The 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:
| Parameter | Type | Description |
|---|---|---|
n | int | The 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:
| Parameter | Type | Description |
|---|---|---|
n | int | The 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:
| Parameter | Type | Description |
|---|---|---|
n | int | The 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:
| Parameter | Type | Description |
|---|---|---|
p | int | The modulus |
base | int | The base |
target | int | The 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:
| Parameter | Type | Description |
|---|---|---|
moduli | list[int] | List of moduli (need not be pairwise coprime) |
residues | list[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:
| Parameter | Type | Description |
|---|---|---|
a | int | Numerator |
n | int | Denominator (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:
| Parameter | Type | Description |
|---|---|---|
n | int | A positive integer |
Returns: $\varphi(n)$ (arbitrary precision).
Example:
>>> ocas.totient(12)
4
mobius
ocas.mobius(n: int) -> int
Möbius function $\mu(n)$.
Parameters:
| Parameter | Type | Description |
|---|---|---|
n | int | A 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:
| Parameter | Type | Description |
|---|---|---|
n | int | A 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:
| Parameter | Type | Description |
|---|---|---|
n | int | A positive integer |
k | int | The 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:
| Parameter | Type | Description |
|---|---|---|
n | int | A 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 areocas.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:
| Parameter | Type | Description |
|---|---|---|
generators | list | List of generators (MultivariatePolynomial or Polynomial objects) |
n_vars | int | Number of variables (default 1; auto-detected if the generators are MultivariatePolynomial) |
algorithm | str | Algorithm 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:
| Parameter | Type | Description |
|---|---|---|
generators | list | List of generators of the ideal |
f | MultivariatePolynomial or Polynomial | The polynomial to test |
n_vars | int | Number of variables (default 1) |
algorithm | str | Algorithm 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:
| Parameter | Type | Description |
|---|---|---|
equations | list | List of equations (each a polynomial in the form equal to zero) |
n_vars | int | Number of variables (default 1) |
algorithm | str | Algorithm 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:
| Parameter | Type | Description |
|---|---|---|
gb | GroebnerBasis | A 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:
| Parameter | Type | Description |
|---|---|---|
generators | list | List of generators of the ideal |
n_vars | int | Number 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:
| Parameter | Type | Description |
|---|---|---|
generators | list | List of generators of the ideal |
n_vars | int | Number 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:
| Parameter | Type | Description |
|---|---|---|
gb | GroebnerBasis | A 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:
| Parameter | Type | Description |
|---|---|---|
generators | list | List of generators of the ideal |
elim_vars | int | Number of variables to eliminate (from the front) |
n_vars | int | Total number of variables (default 1) |
algorithm | str | Algorithm 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:
| Attribute | Type | Description |
|---|---|---|
n_vars | int | Number of variables |
Methods:
| Method | Returns | Description |
|---|---|---|
__len__() | int | Number of polynomials in the basis |
is_groebner_basis() | bool | Verifies whether it is indeed a Gröbner basis |
RealSolution
A real solution of a polynomial system.
Attributes:
| Attribute | Type | Description |
|---|---|---|
values | list[float] | Coordinates of the solution |
multiplicity | int | The multiplicity |
PolynomialSystemSolution
The solving result of a polynomial system.
Attributes:
| Attribute | Type | Description |
|---|---|---|
kind | str | "zero_dimensional", "positive_dimensional", or "empty" |
vector_space_dimension | int or None | Dimension of the vector space (only set for the zero-dimensional case) |
Methods:
| Method | Returns | Description |
|---|---|---|
solutions() | list[RealSolution] | List of real solutions (non-empty only in the zero-dimensional case) |
HilbertSeries
The Hilbert series.
Attributes:
| Attribute | Type | Description |
|---|---|---|
dimension | int | Krull dimension |
degree | int | Multiplicity (degree) |
numerator | list[int] | Coefficients of the numerator polynomial |
Methods:
| Method | Parameters | Returns | Description |
|---|---|---|---|
hilbert_function(d) | d: int | int | Value of the Hilbert function at degree $d$ |
PrimaryComponent
A component of a primary decomposition.
Attributes:
| Attribute | Type | Description |
|---|---|---|
n_vars | int | Number 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:
| Parameter | Type | Description |
|---|---|---|
min_poly | list[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:
| Parameter | Type | Description |
|---|---|---|
c | int 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:
| Parameter | Type | Description |
|---|---|---|
coeffs | list[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:
| Parameter | Type | Description |
|---|---|---|
field | AlgebraicExtension | The algebraic number field containing the coefficients |
coeffs | list | Coefficient 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:
| Attribute | Type | Description |
|---|---|---|
factor | AlgebraicPolynomial | The irreducible factor |
multiplicity | int | The 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:
| Parameter | Type | Description |
|---|---|---|
n_vars | int | Number 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:
| Parameter | Type | Description |
|---|---|---|
shape | DualShape | The dual-number layout |
i | int | Variable index |
value | int 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:
| Parameter | Type | Description |
|---|---|---|
shape | DualShape | The dual-number layout |
value | int 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
| Operation | Syntax | Description |
|---|---|---|
| Addition | a + b | Component-wise addition |
| Subtraction | a - b | Component-wise subtraction |
| Multiplication | a * b | Product rule |
| Division | a / b | Quotient rule |
| Negation | -a | Component-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:
| Parameter | Type | Description |
|---|---|---|
name | str | Tensor name |
slots | list[tuple[str, str]] | List of index slots, each being (label, position). Position is "upper" (contravariant) or "lower" (covariant) |
symmetry | str | Symmetry: "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:
| Parameter | Type | Description |
|---|---|---|
a | Tensor | The first tensor |
b | Tensor | The second tensor |
Returns: a (kind, payload) tuple:
- when
kind = "product",payloadis the list of tensors after contraction (free indices concatenated) - when
kind = "scalar",payloadis 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:
| Parameter | Type | Description |
|---|---|---|
tensor | Tensor | The 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:
| Parameter | Type | Description |
|---|---|---|
expr | str | The tensor expression string |
specs | dict[str, str] | Tensor name → symmetry specification ("none", "symmetric", "antisymmetric") |
index_groups | dict[str, int] or None | Index 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:
| Parameter | Type | Description |
|---|---|---|
expr | str | The tensor expression string |
tableau | list[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:
| Parameter | Type | Description |
|---|---|---|
expr | str | The tensor expression string |
specs | dict[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:
| Parameter | Type | Description |
|---|---|---|
input | str | The expression string |
param_names | list[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:
| Parameter | Type | Description |
|---|---|---|
values | list[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:
| Parameter | Type | Description |
|---|---|---|
n_dims | int | The integration dimension |
n_bins | int or None | Number of bins per dimension (default 64) |
n_samples | int or None | Number of samples per iteration (default 10000) |
iterations | int or None | Number of iterations (default 10) |
learning_rate | float or None | Grid adaptation learning rate (default 1.5) |
seed | int or None | RNG 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:
| Parameter | Type | Description |
|---|---|---|
f | Callable | Accepts 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:
| Parameter | Type | Description |
|---|---|---|
f | Callable | A unary floating-point function |
a | float | Lower integration limit |
b | float | Upper integration limit |
n_bins etc. | Same as Vegas | Optional 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:
| Attribute | Type | Description |
|---|---|---|
integral | float | The integral estimate |
error | float | The 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:
| Parameter | Type | Description |
|---|---|---|
hi | float | The primary value |
lo | float | The 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
| Operation | Syntax | Description |
|---|---|---|
| Addition | a + b | Double-precision addition |
| Subtraction | a - b | Double-precision subtraction |
| Multiplication | a * b | Double-precision multiplication |
| Division | a / b | Double-precision division |
| Negation | -a | Component-wise negation |
| Absolute value | abs(a) | Double-precision absolute value |
| Power | a ** n | Integer power (n is an int) |
| Comparison | ==, <, <=, >, >= | Total order comparison |
Exceptions: ValueError — division by zero (__truediv__).
DoubleF64 transcendental functions
| Method | Description | Exceptions |
|---|---|---|
sin() | Sine | — |
cos() | Cosine | — |
tan() | Tangent | — |
exp() | Natural exponential $e^x$ | — |
ln() | Natural logarithm | ValueError: non-positive input |
sqrt() | Square root | ValueError: 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
| Function | Signature | Description |
|---|---|---|
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 | None | ax+by=c |
classify_ode | (equation, func, var) -> list[str] | ODE classification |
dsolve | (equation, func, var, hint=None) -> str | Symbolically solves ODEs |
dsolve_ivp | (equation, func, var, y0, y1=None) -> str | Laplace IVP |
factorint | (n) -> list[tuple] | Integer prime factorization |
isprime | (n) -> bool | BPSW primality test |
isprime_u64 | (n) -> bool | Deterministic primality (u64) |
nextprime | (n) -> int | Next prime |
discrete_log | (p, base, target) -> int | Discrete logarithm |
crt | (moduli, residues) -> tuple | Chinese remainder theorem |
jacobi_symbol | (a, n) -> int | Jacobi symbol |
totient | (n) -> int | Euler totient |
mobius | (n) -> int | Möbius function |
divisor_count | (n) -> int | Number of divisors |
divisor_sigma | (n, k=1) -> int | Sum of divisor powers |
liouville_lambda | (n) -> int | Liouville 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) -> HilbertSeries | Hilbert series |
py_ideal_radical | (generators, n_vars=1) | Ideal radical |
py_primary_decomposition | (generators, n_vars=1) | Primary decomposition |
py_is_zero_dimensional | (gb) -> bool | Zero-dimensionality test |
py_eliminate | (generators, elim_vars, n_vars=1, algorithm="auto") | Elimination |
contract_tensors | (a, b) -> tuple | Tensor contraction |
tensor_symmetrise_sign | (tensor) -> int | Symmetrization sign |
canonicalize_tensors | (expr, specs, index_groups=None) | Tensor canonicalization |
young_project | (expr, tableau) -> str | Young projection |
refresh_dummies | (expr, specs) -> str | Dummy index refresh |
integrate_1d | (f, a, b, **opts) -> IntegrateResult | One-dimensional numerical integration |
Class Quick Reference
| Class | Constructor | Description |
|---|---|---|
Expression | Expression(input) | Symbolic expression |
Polynomial | Polynomial(coeffs, domain=None) | Univariate polynomial |
MultivariatePolynomial | MultivariatePolynomial(terms, n_vars) | Multivariate polynomial |
Matrix | Matrix(rows, domain=None) | Matrix over a domain |
IntegerDomain | IntegerDomain() | ℤ selector |
RationalDomain | RationalDomain() | ℚ selector |
FiniteField | FiniteField(modulus) | GF(p) |
AlgebraicExtension | AlgebraicExtension(min_poly) | Algebraic number field |
AlgebraicElement | (created by AlgebraicExtension) | Field element |
AlgebraicPolynomial | AlgebraicPolynomial(field, coeffs) | Polynomial over an algebraic field |
DualShape | DualShape.first_order(n_vars) | Dual-number layout |
HyperDual | HyperDual.variable(...) / .constant(...) | Hyper-dual number |
Tensor | Tensor(name, slots, symmetry="none") | Named tensor |
ExpressionEvaluator | ExpressionEvaluator(input, param_names) | Numerical evaluator |
Vegas | Vegas(n_dims, **opts) | Monte Carlo integrator |
IntegrateResult | IntegrateResult(integral, error) | Integration result |
DoubleF64 | DoubleF64(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.hheader is out of dateThe cbindgen-generated
ocas-c/include/ocas.his 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
-1error code (instead of theOCAS_*constants), and theOcasGroebnerBasisandOcasSystemSolutionhandles 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.hppcovers only three domains (22 wrapper functions in total):
- Expressions: the
ocas::Expressionclass (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_outparameter may beNULL(error code ignored). - Functions returning pointers:
NULLon failure. - Functions returning
int: non-zero on failure (see each function's documentation for the exact meaning). - Functions returning
size_t:0on failure. - Number theory functions returning
intuse the special sentinel values-1(isprime) or-2(jacobi/mobius/liouville) to signal errors.
Memory Management
| Return type | Free function |
|---|---|
char* (string) | ocas_string_free() |
OcasExpr* | ocas_expr_free() |
OcasPolyZ* | ocas_poly_z_free() |
OcasPolyFp* | ocas_poly_fp_free() |
OcasPolyFactorArray | ocas_poly_factor_array_free() (frees only the array structure, not the internal polynomial handles) |
OcasAlgebraicField* | ocas_algebraic_field_free() |
OcasAlgebraicPoly* | ocas_algebraic_poly_free() |
OcasAlgebraicFactorArray | ocas_algebraic_factor_array_free() (as above, frees only the array) |
OcasVegas* | ocas_vegas_free() |
OcasTensor* | ocas_tensor_free() |
OcasTensorContraction | ocas_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:
| Parameter | Type | Description |
|---|---|---|
input | const char* | Null-terminated expression string, e.g. "x^2 + 2*x + 1" |
err_out | int* | 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:
| Parameter | Type | Description |
|---|---|---|
handle | OcasExpr* | 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:
| Parameter | Type | Description |
|---|---|---|
handle | const OcasExpr* | Non-NULL expression handle |
err_out | int* | 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:
| Parameter | Type | Description |
|---|---|---|
handle | const OcasExpr* | Non-NULL expression handle |
err_out | int* | 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:
| Parameter | Type | Description |
|---|---|---|
s | char* | 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:
| Parameter | Type | Description |
|---|---|---|
handle | OcasExpr* | Non-NULL expression handle (modified) |
err_out | int* | 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:
| Parameter | Type | Description |
|---|---|---|
handle | const OcasExpr* | Non-NULL expression handle |
var | const char* | Variable name, e.g. "x" |
err_out | int* | 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:
| Parameter | Description |
|---|---|
handle | The integrand expression |
var | The integration variable name |
outcome_out | Output for the three-valued verdict; may be NULL |
err_out | Error 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.hheader. 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:
| Parameter | Type | Description |
|---|---|---|
handle | const OcasExpr* | Non-NULL expression handle |
var | const char* | Expansion variable |
point | const OcasExpr* | Expansion point (expression handle) |
order | uint32_t | Expansion order |
err_out | int* | 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:
| Parameter | Type | Description |
|---|---|---|
handle | const OcasExpr* | Non-NULL expression handle |
err_out | int* | 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:
| Parameter | Type | Description |
|---|---|---|
handle | const OcasExpr* | Non-NULL expression handle |
var | const char* | Name of the variable to replace |
replacement | const OcasExpr* | Replacement expression (non-NULL) |
err_out | int* | 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++ method | Corresponding 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:
| Parameter | Type | Description |
|---|---|---|
input | const char* | Polynomial string, e.g. "x^2*y + 3*x - 1" |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
poly | const OcasPolyZ* | Non-NULL polynomial handle |
out | OcasPolyFactorArray* | Output struct (filled on success) |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
input | const char* | Polynomial string |
prime | const char* | Decimal string of the prime $p$ |
err | int* | 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].polywithocas_poly_z_free()/ocas_poly_fp_free()before calling this function. Doing it in the wrong order causes a use-after-free.
Parameters:
| Parameter | Type | Description |
|---|---|---|
arr | OcasPolyFactorArray* | 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:
| Parameter | Type | Description |
|---|---|---|
min_poly | const char* | Minimal polynomial string, e.g. "x^2 - 2" |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
field | const OcasAlgebraicField* | Non-NULL field handle |
coeffs | const char* | Coefficient list string (see format above) |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
poly | const OcasAlgebraicPoly* | Non-NULL polynomial handle |
out | OcasAlgebraicFactorArray* | Output array (filled on success) |
err | int* | 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, thexargument of the integrand represents the first dimension only; a full multi-dimensionalconst double*/size_tsignature 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:
| Parameter | Type | Description |
|---|---|---|
n_dims | size_t | Number of integration dimensions |
opts | const OcasVegasOptions* | May be NULL (uses defaults) |
err | int* | May be NULL |
OcasVegasOptions fields:
| Field | Type | Default | Description |
|---|---|---|---|
n_bins | size_t | 64 | Number of bins per dimension |
n_samples | size_t | 10000 | Samples per iteration |
iterations | size_t | 10 | Number of adaptive iterations |
learning_rate | double | 1.5 | Grid smoothing/learning rate |
seed | uint64_t | 0x0C45 | Random 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:
| Parameter | Type | Description |
|---|---|---|
v | OcasVegas* | Non-NULL integrator handle |
f | ocas_integrand_t | Integrand function |
user_data | void* | User data passed to the integrand |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
f | ocas_integrand_t | Integrand |
user_data | void* | User data |
a | double | Lower integration limit |
b | double | Upper integration limit |
opts | const OcasVegasOptions* | May be NULL |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
equation | const char* | Expression equal to zero, e.g. "Derivative(y(x), x) - y(x)" |
func | const char* | Unknown function name, e.g. "y" |
var | const char* | Independent variable, e.g. "x" |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
equation | const char* | ODE expression |
func | const char* | Unknown function name |
var | const char* | Independent variable |
hint | const char* | May be NULL (automatic classification); or one of the method names returned by ocas_ode_classify |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
equation | const char* | ODE expression |
func | const char* | Unknown function name |
var | const char* | Independent variable |
y0 | const char* | String expression for $y(0)$, e.g. "1" |
y1 | const char* | $y'(0)$ (may be NULL; only needed for second order) |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
n | const char* | Decimal integer string |
err_out | int* | 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:
| Parameter | Type | Description |
|---|---|---|
moduli | const char* | Comma-separated moduli, e.g. "3,5,7" |
residues | const 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:
| Parameter | Type | Description |
|---|---|---|
n | const char* | Positive integer |
k | uint32_t | Power |
err_out | int* | 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++ function | C function | Description |
|---|---|---|
factorint(n) | ocas_ntheory_factorint | Prime factorization |
isprime(n) | ocas_ntheory_isprime | Returns bool; throws on error |
nextprime(n) | ocas_ntheory_nextprime | Next prime |
discrete_log(p, base, target) | ocas_ntheory_discrete_log | Discrete logarithm; throws if no solution |
crt(moduli, residues) | ocas_ntheory_crt | Chinese remainder theorem |
jacobi(a, n) | ocas_ntheory_jacobi | Jacobi symbol; throws on invalid input |
totient(n) | ocas_ntheory_totient | Euler totient |
mobius(n) | ocas_ntheory_mobius | Möbius function; throws on invalid input |
divisor_count(n) | ocas_ntheory_divisor_count | Number of divisors |
divisor_sigma(n, k=1) | ocas_ntheory_divisor_sigma | Sum of divisor powers |
liouville(n) | ocas_ntheory_liouville | Liouville 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:
| Parameter | Type | Description |
|---|---|---|
name | const char* | Tensor name, e.g. "T" |
slots | const char* | Slot string |
symmetry | const char* | May be NULL (= "none"); or "none"/"symmetric"/"antisymmetric" |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
a | const OcasTensor* | Non-NULL tensor |
b | const OcasTensor* | Non-NULL tensor |
out | OcasTensorContraction* | Output struct |
err | int* | May be NULL |
Return value: OCAS_OK (0) on success.
OcasTensorContraction fields:
| Field | Type | Description |
|---|---|---|
kind | int | 0 = product (free indices remain), 1 = scalar (fully contracted) |
tensors | OcasTensor** | Valid when kind == 0; array of tensor handles |
n_tensors | size_t | Length of the tensors array |
scalar_str | char* | Valid when kind == 1; string of the scalar result |
Memory:
kind == 0: free eachtensors[i]withocas_tensor_free(), and the array withocas_tensor_contraction_free().kind == 1: freescalar_strwithocas_string_free(), and the struct withocas_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:
| Parameter | Type | Description |
|---|---|---|
expr_str | const char* | Tensor product string, e.g. "T(i,j)*U(j,k)" |
specs_str | const char* | Comma-separated name:sym pairs, e.g. "T:none,U:none" |
groups_str | const char* | May be NULL; or comma-separated label:group pairs |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
expr_str | const char* | Tensor expression, e.g. "f(a,b)" |
tableau_str | const char* | Comma-separated row lengths, e.g. "2,1" denotes □□/□ |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
expr_str | const char* | Tensor expression |
specs_str | const char* | name:sym pairs |
err | int* | 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:
| Parameter | Type | Description |
|---|---|---|
shape | const OcasDualShape* | Non-NULL shape handle |
i | size_t | Variable index ($0 \le i < \text{n_vars}$) |
coeff | const char* | Coefficient string "num" or "num/den" |
err | int* | 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.hheader; declare the prototypes yourself.- Error codes use
-1instead of theOCAS_*constants.- The
OcasGroebnerBasisandOcasSystemSolutionhandles are not marked#[repr(C)].
Polynomial data format
Polynomials are passed as arrays of coefficients; each polynomial is specified by:
n_vars— number of variablesn_terms— number of termsexponents— 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:
| Parameter | Type | Description |
|---|---|---|
n_polys | size_t | Number of polynomials |
n_vars_array | const size_t* | Number of variables for each polynomial |
n_terms_array | const size_t* | Number of terms for each polynomial |
exponents | const size_t* | Flattened exponent matrix |
coeff_nums | const int64_t* | Numerators of the rational coefficients |
coeff_dens | const int64_t* | Denominators of the rational coefficients |
algorithm | int32_t | Algorithm selection: 0 = Auto, 1 = F4, 2 = F5, 3 = Buchberger |
err | int32_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:
| Parameter | Type | Description |
|---|---|---|
sol | const OcasSystemSolution* | Non-NULL solution handle |
sol_idx | size_t | Index of the solution |
var_idx | size_t | Index 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
- Rust API reference — overview of the Rust public API
- Python API reference — Python binding documentation
- Architecture — overview of the project architecture
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
Recommended Learning Paths
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}$$
| Step | Topic | Key Takeaways | File |
|---|---|---|---|
| 1 | Polynomial Algebra | Polynomial rings, monomial orders, the division algorithm | polynomial-algebra.md |
| 2 | Linear Algebra | Matrix operations, the Bareiss algorithm, Gaussian elimination | linear-algebra.md |
| 3 | Finite Fields & Modular Arithmetic | Construction of $\mathbb{F}_p$, modular inverses, NTT | finite-fields.md |
| 4 | Polynomial GCD & Factorization | Euclid's algorithm, Hensel lifting, Berlekamp's algorithm | poly-gcd-factoring.md |
| 5 | Gröbner Basis Theory | Buchberger's algorithm, F4/F5, Hilbert functions | groebner-theory.md |
| 6 | FGLM & Elimination Theory | Order-change algorithms, ideal operations, primary decomposition | fglm-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}$$
| Step | Topic | Key Takeaways | File |
|---|---|---|---|
| 1 | Finite Fields & Modular Arithmetic | Construction of $\mathbb{F}p$ and $\mathbb{F}{p^d}$, cyclicity of the multiplicative group | finite-fields.md |
| 2 | Number Theory Algorithms | BPSW primality, ECM factorization, BSGS discrete logarithms, Tonelli–Shanks | number-theory-algorithms.md |
| 3 | Algebraic Number Fields & Galois Theory | Representation 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}$$
| Step | Topic | Key Takeaways | File |
|---|---|---|---|
| 1 | Symbolic Calculus | Differentiation rules, Taylor expansion, expression-tree transformations | symbolic-calculus.md |
| 2 | ODE Solving Theory | Separable/linear/Bernoulli/constant-coefficient/series solutions/Laplace transforms | ode-theory.md |
| 3 | The Risch Integration Algorithm | Differential-field towers, Hermite reduction, the logarithmic derivative identity, RDE | risch-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.
| Topic | Level | oCAS Module | Math Chapter |
|---|---|---|---|
| Polynomial Algebra | Foundations | ocas-poly (dense.rs, sparse.rs) | polynomial-algebra.md |
| Finite Fields & Modular Arithmetic | Foundations | ocas-domain (finite_field.rs) | finite-fields.md |
| Linear Algebra | Foundations | ocas-poly (matrix.rs) | linear-algebra.md |
| Polynomial GCD & Factorization | Advanced | ocas-poly (gcd/, factor/) | poly-gcd-factoring.md |
| Gröbner Basis Theory | Advanced | ocas-poly (groebner/mod.rs, f4.rs, f5.rs, hilbert.rs) | groebner-theory.md |
| Number Theory Algorithms | Advanced | ocas-domain (number_theory/) | number-theory-algorithms.md |
| Symbolic Calculus | Advanced | ocas-calc (lib.rs) | symbolic-calculus.md |
| ODE Solving Theory | Advanced | ocas-calc (ode/) | ode-theory.md |
| The Risch Integration Algorithm | Higher | ocas-calc (integral/) | risch-algorithm.md |
| FGLM & Elimination Theory | Higher | ocas-poly (groebner/fglm.rs, ideal.rs) | fglm-elimination.md |
| Algebraic Number Fields & Galois Theory | Higher | ocas-domain (algebraic.rs) | algebraic-number-fields.md |
| Tensor Algebra & Canonicalization | Higher | ocas-atom (tensor/canon.rs, young.rs, spec.rs) | tensor-canonicalization.md |
| Monte Carlo Integration | Higher | ocas-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:
| Operation | Degree 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:
| Representation | Storage | Use cases |
|---|---|---|
| Dense representation | Coefficient vector $[a_0, a_1, \ldots, a_n]$ | Univariate, moderate degrees |
| Sparse representation | Stores 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:
- Well-ordering: every nonempty subset of $\mathbb{N}^n$ has a least element (equivalently: $0 \preceq \alpha$ for all $\alpha$)
- 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:
- $|\alpha| > |\beta|$, or
- $|\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:
- $|\alpha| > |\beta|$, or
- $|\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
| Monomial | Lex | Grlex | Grevlex |
|---|---|---|---|
| $x^2$ ($\alpha = (2,0)$) | 1 | 2 | 2 |
| $xy$ ($\alpha = (1,1)$) | 2 | 3 | 3 |
| $y^3$ ($\alpha = (0,3)$) | 3 | 1 (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_remimplements this by performing division with remainder on the leading coefficients viaEuclideanDomain::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:
- Termination: guaranteed by the well-ordering of the monomial order
- Non-uniqueness: the remainder $r$ depends on the order of division $g_1, \ldots, g_s$. Different orders may produce different remainders
- 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
domainfield is stored inside the polynomial, avoiding the need to pass it to every operation - Polynomial type parameter: the
D: Domaingeneric 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:
| Condition | Algorithm | Complexity |
|---|---|---|
| $\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
multakes one of the first two rows (Karatsuba/Schoolbook; the thresholdKARATSUBA_THRESHOLD = 32is applied to the number of coefficients, i.e. degree $\geq 31$). The NTT is enabled throughDenseUnivariatePolynomial<FiniteField>::mul_ntt, which internally checksNTT_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, returnsOption<(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 factorizationresultant(&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,Grevlexare zero-sized types (ZSTs) occupying no runtime space;WeightOrder,MatrixOrder,BlockOrdercarry 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(°_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(°_lhs) .then_with(|| lhs.cmp(rhs)) } } }
Implementation detail:
deg_rhs.cmp(°_lhs)rather thandeg_lhs.cmp(°_rhs)— because in Rust'sOrdering,Lessmeans "comes first", and we need the higher degree to come first (i.e.Less), so we comparedeg_rhsagainstdeg_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
| Consideration | Dense DenseUnivariatePolynomial | Sparse SparseMultivariatePolynomial |
|---|---|---|
| Number of variables | Univariate | Multivariate ($n$ variables) |
| Storage | Contiguous Vec, $O(\deg f + 1)$ | HashMap, $O(\text{number of nonzero terms})$ |
| Multiplication | Karatsuba / NTT | Term-by-term expansion |
| Division | Unique quotient and remainder | Depends on the monomial order; remainder not unique |
| GCD | Euclid's algorithm / PRS | Requires Gröbner basis techniques |
| Supported coefficient domains | EuclideanDomain | Domain (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
-
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
-
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
-
Adams, W. W., Loustaunau, P. An Introduction to Gröbner Bases. AMS, 1994.
- Chapter 1: monomial orders and multivariate division
See also
- Gröbner Basis Theory — the solution to the non-uniqueness of division
- Polynomial GCD and Factorization — univariate and multivariate GCD algorithms
- Finite Fields and Modular Arithmetic — polynomial arithmetic over finite fields
- Linear Algebra — Bareiss's fraction-free determinant and Gaussian elimination
- Rust API: Polynomials — the complete API of
DenseUnivariatePolynomialandSparseMultivariatePolynomial - Rust API: Coefficient Domains — the
DomainandEuclideanDomaintraits
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:
- Initialize: $(r_0, r_1) = (a, n)$, $(s_0, s_1) = (1, 0)$, $(t_0, t_1) = (0, 1)$.
- 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} $$
- 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:
- Pick a degree-$d$ irreducible polynomial $m(x)$ in $\mathbb{F}_p[x]$.
- Define $\mathbb{F}_{p^d} = \mathbb{F}_p[x] / (m(x))$ — the quotient of the polynomial ring by the ideal $(m(x))$.
- 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$.
- 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
primefield stores the modulus $p$ as anum-bigintarbitrary-precision integer. prime_minus_twois precomputed at construction time so it does not have to be recomputed on every inversion.- With the
gmpfeature enabled, the GMP representations are cached as well, exploitingrug'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:
| Operation | Method | Implementation strategy |
|---|---|---|
| Addition | f.add(&a, &b) | $(a + b) \bmod p$ |
| Subtraction | f.sub(&a, &b) | $(a - b) \bmod p$ (mod_floor handles negatives) |
| Multiplication | f.mul(&a, &b) | $(a \cdot b) \bmod p$ |
| Negation | f.neg(&a) | $(-a) \bmod p$ |
| Inversion | f.inv(&a) | $a^{p-2} \bmod p$ (Fermat's little theorem); None when $a = 0$ |
| Division | f.div(&a, &b) | $a \cdot b^{-1}$; None when $b = 0$ |
| Exponentiation | f.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$):
- Zero padding: pad the coefficient vectors of $f, g$ to length $N = 2^{\lceil \log_2(2n) \rceil}$.
- Forward transform: $\hat{f} = \mathrm{NTT}(f)$, $\hat{g} = \mathrm{NTT}(g)$.
- Pointwise multiplication: $\hat{h}_i = \hat{f}_i \cdot \hat{g}_i$ (component-wise modulo $p$).
- 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 % poperations. 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
u64are supported ($p < 2^{64}$, checked intry_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:
- $t = a \cdot b$ (128-bit intermediate value)
- $m = (t \bmod R) \cdot p' \bmod R$
- $u = (t + m \cdot p) / R$
- 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
- 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.
- Lidl, R. & Niederreiter, H. Introduction to Finite Fields and Their Applications. Cambridge University Press, 1994. — The classical textbook on finite-field theory.
- 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).
- 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, andrem - 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
- Multiplicativity: $\det(AB) = \det(A) \cdot \det(B)$
- Transpose invariance: $\det(A^\top) = \det(A)$
- Row swap changes sign: swapping two rows changes the sign of the determinant
- Row scaling: multiplying a row by $c$ multiplies the determinant by $c$
- Row addition invariance: adding a multiple of one row to another leaves the determinant unchanged
- Singularity test: $\det(A) = 0$ if and only if $A$ is not invertible (singular)
- 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$:
-
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$.
-
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} $$
- 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:
- All nonzero rows are above all zero rows
- The column of each pivot is all zeros below it
- 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
| Method | Time complexity | Space complexity | Fraction explosion | Domain compatibility |
|---|---|---|---|---|
| Laplace expansion | $O(n!)$ | $O(n^2)$ (recursion depth $O(n)$) | None | Any commutative ring (infeasible in practice) |
| Classical Gaussian elimination | $O(n^3)$ | $O(n^2)$ | Severe | Fields only |
| Bareiss | $O(n^3)$ | $O(n^2)$ | None | Euclidean 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:
- For each column $j$, select the first nonzero element in the current column as the pivot (skip the column if it is entirely zero)
- Content stripping: compute the GCD of the pivot row and divide the whole row by it
- 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:
- Initialization: copy the data into a working array, $p_0 = 1$, sign flag $\epsilon = 1$
- 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)}$
- 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:
- Augmentation: append $b$ to form $[A \mid b]$
- Elimination:
row_echelon(nvars)to reach row echelon form - Consistency check: if there is a nonzero $b$ component after the pivot rows, the system is inconsistent and
MatrixError::Inconsistentis returned - Back-substitution: fraction-free
back_substitution(nvars) - Rank check: if the rank $< n$, the system is underdetermined and
MatrixError::Underdeterminedis returned - Final division: divide each solution component by its corresponding pivot
Error Types
| Error | Meaning |
|---|---|
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 |
Inconsistent | The 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) |
ResultNotInDomain | The 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:
- Basic: Polynomial Algebra — polynomial rings, degrees, monomial orders
- Basic: Finite Fields and Modular Arithmetic — construction of $\mathbb{F}_p$, modular inverse, CRT
- Basic: Linear Algebra — matrix operations, determinants
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:
- $d \mid f$ and $d \mid g$ (common divisor)
- 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:
- Evaluate: choose $\alpha \in \mathbb{Z}$ and compute $f(x, \alpha)$ and $g(x, \alpha)$ (univariate polynomials)
- Univariate GCD: $d_\alpha(x) = \gcd(f(x, \alpha), g(x, \alpha))$
- Verify: check that the degree of $d_\alpha(x)$ equals the degree of the true GCD (take the minimal consistent degree over several $\alpha$)
- 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:
- Choose an evaluation point $\alpha$ for $x_n$; compute $f|{x_n = \alpha}$ and $g|{x_n = \alpha}$
- Recursively compute the GCD in $n-1$ variables
- 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:
- 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
- Compute the multivariate GCD of the primitive parts
- 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:
- Evaluate: choose evaluation points for the auxiliary variables to obtain a univariate image $f(x_1, \alpha_2, \ldots, \alpha_n)$
- Univariate factorization: factor the univariate image over $\mathbb{Z}$ (or $\mathbb{F}_p$)
- Variable-by-variable Hensel lifting: lift back to the multivariate factorization one variable at a time
- 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:
- Factor $\ell$ into powers of irreducible factors: $\ell = \prod g_j^{e_j}$
- At the evaluation point $\alpha = (\alpha_2, \ldots, \alpha_n)$, the image of $\ell$ splits into the images of the univariate factors
- 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)
- 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):
- 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$
- Compute Bézout coefficients $b_i$ such that $\sum b_i \prod_{j \ne i} u_j = 1$
- 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$
- 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:
- Reduce the coefficients of the multivariate factors modulo $p$ to obtain a skeleton over $\mathbb{F}_p$
- Iteratively solve the mod-$p$ Diophantine equations, lifting the coefficients step by step from $\bmod p$ to $\bmod p^k$
- 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:
- Extract the skeleton from the terms of the error $e$ — the possible exponent patterns of the correction terms
- Evaluate at several random base points to obtain univariate Diophantine equations
- Interpolate the coefficients through a Vandermonde system
- 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:
- 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$
- 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
- 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):
- Choose a prime $p$ such that $m$ is irreducible over $\mathbb{F}_p$ (ensuring $\mathbb{F}_p[\alpha]/(m) \cong \text{GF}(p^d)$)
- Map $a, b \in K[x]$ to $\text{GF}(p^d)[x]$
- Compute the monic GCD over $\text{GF}(p^d)[x]$
- 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 file | Algorithm | Description |
|---|---|---|
ocas-poly/src/dense.rs | Euclidean GCD, Karatsuba multiplication | Core data structure for univariate polynomials |
ocas-poly/src/gcd/modular.rs | Brown modular GCD | Efficient GCD over $\mathbb{Z}[x]$ |
ocas-poly/src/factor/mod.rs | Top-level entry, Yun square-free factorization | factor() and square_free_factorization() |
ocas-poly/src/factor/finite_field.rs | Berlekamp, Cantor–Zassenhaus (DDF + EDF) | Factorization over $\mathbb{F}_p[x]$ |
ocas-poly/src/factor/hensel.rs | Hensel lifting, Zassenhaus recombination, Mignotte bound | The $\mathbb{Z}[x]$ factorization pipeline |
ocas-poly/src/factor/multivariate.rs | Bivariate 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.rs | General 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.rs | Trager's algorithm (factorization over algebraic number fields) | $\mathbb{Q}(\alpha)[x]$ |
ocas-poly/src/multivariate_gcd.rs | Multivariate 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
| Algorithm | Time complexity | Space complexity | Use 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 EEZ | Exponential in the number of variables | Multivariate polynomials | Multivariate 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:
- 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)
- Equality testing of ideals: two ideals satisfy $I = J$ if and only if their (reduced) Gröbner bases coincide
- Elimination: a Gröbner basis under the Lex order naturally has the elimination property (generators of the elimination ideal appear in the basis)
- 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:
- each $g_i$ is monic ($\text{lc}(g_i) = 1$)
- 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 —
- Number of critical pairs: $O(|G|^2)$ critical pairs, most of which are redundant
- One at a time: only one S-polynomial is reduced per step; no batching is exploited
- 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}$:
- Generate candidate pairs: $P_{\text{new}} = {(f_{\text{new}}, f_i) \mid 1 \leq i \leq m}$
- First-criterion filtering: delete the pairs where $\text{lm}(f_{\text{new}})$ and $\text{lm}(f_i)$ are coprime
- 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)
- Redundant pair cleanup: mark old pairs whose $\text{lcm}$ is divisible by the $\text{lcm}$ of a new pair as redundant
- 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:
- Select critical pairs: take a batch of critical pairs from the queue (sorted by $\text{lcm}$)
- 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
- Build the column map: order all monomials that occur and number them $0, 1, \ldots, N-1$
- 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$:
- Sort the rows by their first nonzero column
- 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
- 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$:
- if the leading term of $h_r$ is "new" (not in the set of leading terms collected so far), add it to the basis
- 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:
- Incremental processing: add the generators $f_1, f_2, \ldots, f_s$ one at a time
- Labeled S-polynomials: every S-polynomial enters the matrix carrying its signature
- 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)$
- 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:
| Task | Order needed |
|---|---|
| Elimination | Lex |
| Solving polynomial systems | Lex (combined with Sturm root isolation) |
| Ideal membership testing | Any order works |
| Efficient computation | Grevlex (smallest intermediate polynomials) |
The standard strategy is "compute fast, then convert":
- Compute the basis with Grevlex (fastest — the intermediate polynomials have the lowest degrees)
- 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:
- Generate candidates: pair the new polynomial with every existing element of the basis
- First criterion (
monomial_are_coprime): if the leading monomials are coprime, skip directly (the product criterion) - 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
- 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):
- compute $m = \text{mask}(\text{exp})$
- enumerate all submasks $s \subseteq m$ of $m$
- for each basis element $g$ in
buckets[s], check exactly whether $\text{lm}(g) \mid x^\text{exp}$ - 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:
- Select critical pairs: take pairs from the queue sorted by $\text{lcm}$ size
- Symbolic preprocessing: expand the S-polynomial of each pair and all reducers, and build the column map
- Row echelonization:
echelonize_fp(ℤ_p fast path) orechelonize_generic(generic) - Extract new elements: convert the nonzero rows back to polynomials and check whether the leading term is "new"
- Update basis and pairs: add the new elements to the basis, and let
update_pairsupdate 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:
- Select pairs: critical pair selection similar to F4 (shares the
update_pairsimplementation) - Syzygy filtering: before a pair enters the matrix, check its signature — if it is a syzygy, skip it
- Labeled rows: each row carries
(Signature, Vec<(coefficient, column)>) - Symbolic preprocessing: the same column-map construction as in F4, but the signatures of reducers must propagate
- Row echelonization: sort the rows in ascending signature order and run Gaussian elimination
- Extract new elements: nonzero rows are extracted as
LabeledPoly, keeping their signatures - 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:
- Extract leading terms: take the leading exponent vector of each polynomial in the basis
- Inclusion–exclusion: call
hilbert_numeratorto obtain $N(t)$ - 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:
| Method | Computation |
|---|---|
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:
- start a BFS at $1 = x_1^0 \cdots x_n^0$
- for each monomial $m$, check whether it is divisible by some $\text{lm}(g_i)$
- monomials that are not divisible are added to the staircase, and all their "neighbors" ($m \cdot x_i$) enter the queue
- 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:
| Variant | Behavior |
|---|---|
Auto | currently routes to F4; in the future the F5 crossover point will be calibrated on cyclic-n benchmarks |
Buchberger | classical S-polynomial iteration (for teaching) |
F4 | batched matrix row echelon |
F5 | signature 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:
| Benchmark | Buchberger | F4 | Notes |
|---|---|---|---|
| cyclic-3 ($\mathbb{Q}$) | feasible | fast | Buchberger is already slow at this size |
| cyclic-4 ($\mathbb{Q}$) | too slow | feasible | only F4 is practical |
| cyclic-3/4/5 ($\mathbb{Z}{13}$, $\mathbb{Z}{101}$) | — | fast | the 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:
- No redundant reductions: for regular sequences, no row of the matrix reduces to zero
- Completeness: it produces exactly the same reduced Gröbner basis as F4
- 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:
- Early termination: the regularity bound $\text{reg}(M)$ provides a reliable degree bound for F4
- Dimension testing: the Krull dimension of $R/I$ equals the order of the pole of $\text{HS}(t)$ at $t=1$
- Degree computation: the degree of the projective variety equals $N(1)$ (for zero-dimensional ideals, the staircase size)
- 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
-
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
-
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
-
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
-
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
-
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
-
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
-
Eder, C. & Perry, J. "Signature-based Algorithms to Compute Gröbner Bases." ISSAC 2009, pp. 139–146.
- Eder–Perry's formalization of F5
-
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
-
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:
- Polynomial Algebra — monomial orders, the foundations of the multivariate division algorithm
- Linear Algebra — the Bareiss algorithm and Gaussian elimination
- FGLM and Elimination Theory — detailed analysis of FGLM and ideal operations
- Gröbner Basis Implementation — usage guide, benchmarks, and the ideal operations API
- Rust API: Gröbner Bases and Ideals — function signatures and complete examples
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
| Function | Definition | Multiplicative |
|---|---|---|
| $\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:
- 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)
- Batched GCD: accumulate the product of several $(x_i - y_i)$ values and take a single $\gcd$, amortizing the expensive GCD
- 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):
- One Pollard rho–Brent attempt first (cheapest for small factors; no smoothness requirement)
- 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$.
- Baby steps: compute and store ${(j, g^j \bmod p) : 0 \leq j < m}$ in a HashMap
- 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
- 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).
- 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$.
- Update $h \leftarrow h \cdot g^{-x_0}$, and compute $h_1 = (h \cdot g^{-x_0})^{n/q^2}$. Find $x_1$.
- Continue until $x_{e-1}$.
- $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}$:
- Compute $g = \gcd(m_1, m_2)$
- Check $r_1 \equiv r_2 \pmod{g}$ (the consistency condition)
- If inconsistent, return
None - 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:
- Decompose: write $p - 1 = Q \cdot 2^S$ ($Q$ odd)
- Find a non-residue: search for $z$ with $\left(\frac{z}{p}\right) = -1$
- 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$
- 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$
- 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)$:
- Initialize: $(r_0, r_1) = (m, a)$, $(t_0, t_1) = (0, 1)$
- 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)$
- Termination condition: stop when $|r_1| \leq \sqrt{m/2}$ and $|t_1| \leq \sqrt{m/2}$
- 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$
- 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
| Function | Location | Description |
|---|---|---|
is_prime(n) | number_theory.rs | 12-base Miller–Rabin; deterministic for $n < 3.317 \times 10^{24}$ |
is_prime_bpsw(n) | primes.rs | base-2 MR + strong Lucas PRP |
is_prime_u64(n) | primes.rs | u64-specific; delegates to is_prime |
next_prime(n) | number_theory.rs | test odd numbers one by one starting from $n + 1$ |
primes_from(n) | number_theory.rs | a 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
| Function | Location | Description |
|---|---|---|
factor_integer(n) | factor.rs | entry point: trial division + recursive splitting |
factor_integer_with_rng(n, rng) | factor.rs | version with an explicit RNG |
factor_trial(n, limit) | factor.rs | trial division, returns (factor, cofactor) |
pollard_rho_brent(n, rng) | factor.rs | Brent variant, with retries |
pollard_pm1(n, b1, rng) | factor.rs | Pollard $p - 1$, Stage 1 |
williams_pp1(n, b1, rng) | factor.rs | Williams $p + 1$, Stage 1 |
ecm(n, b1, max_curves, rng) | factor.rs | Lenstra ECM, Suyama parameterization |
Internal implementation details:
primes_up_to(limit): the sieve of Eratosthenes, generating prime tables for trial division and smoothness boundsprime_power_le(q, bound): computes $q^e \leq \text{bound}$, used to construct the smooth exponent $M$ProjPoint { x, z }: a Montgomery projective-coordinate pointecm_double,ecm_add,ecm_mul: group operations on the Montgomery curvesuyama_curve(sigma, n): the Suyama parameterization, returning theSuyamaenum (Curve/Factor/Degenerate)find_factor(n, rng): the escalation driver (rho → p−1 → p+1 → ECM, with increasing smoothness bounds)
Discrete Logarithms
| Function | Location | Description |
|---|---|---|
dlog_bsgs(base, target, modulus) | dlog.rs | baby-step giant-step |
dlog_pohlig_hellman(base, target, p) | dlog.rs | Pohlig–Hellman (requires $p$ prime) |
Internals:
bsgs_bounded(base, target, modulus, order_bound): the BSGS core, searching with a given order bounddlog_bsgssets the bound tomodulus - 1and delegates tobsgs_boundeddlog_pohlig_hellmanfactors $p - 1$ withfactor_integer, recovers the digits prime power by prime power with BSGS, and combines them withcrt_many
The Chinese Remainder Theorem
| Function | Location | Description |
|---|---|---|
crt(r1, m1, r2, m2) | number_theory.rs | pairwise combination; the moduli need not be coprime |
crt_many(congruences) | crt.rs | left-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
| Function | Location | Description |
|---|---|---|
legendre(a, p) | number_theory.rs | the Legendre symbol, delegating to jacobi |
jacobi(a, n) | number_theory.rs | the Jacobi symbol (quadratic reciprocity + 2-adic stripping) |
mod_sqrt(a, p) | number_theory.rs | Tonelli–Shanks, including the $p \equiv 3 \pmod{4}$ fast path |
The mod_sqrt implementation:
- First check $\left(\frac{a}{p}\right) = 1$ (otherwise return
None) - If $p \equiv 3 \pmod{4}$, take the fast path $x = a^{(p+1)/4} \bmod p$
- 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
| Function | Location | Description |
|---|---|---|
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
| Function | Location | Description |
|---|---|---|
rational_reconstruction(a, m) | ocas-poly/src/rational_reconstruction.rs | the 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
| Function | Location | Description |
|---|---|---|
mod_inv(a, m) | number_theory.rs | modular inverse via the extended Euclidean algorithm |
extended_gcd(a, b) | number_theory.rs | returns $(g, x, y)$ with $ax + by = g$ |
symmetric_mod(a, m) | number_theory.rs | reduce into $(-m/2, m/2]$ |
References
-
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)
-
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
-
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
-
Williams, H. C. "A p+1 method of factoring." Mathematics of Computation, 39(159):225–234, 1982.
- Williams's $p + 1$ method
-
Lenstra, H. W. Jr. "Factoring integers with elliptic curves." Annals of Mathematics, 126(3):649–673, 1987.
- The ECM method
-
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
-
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
-
Baillie, R. & Wagstaff, S. S. "Lucas pseudoprimes." Mathematics of Computation, 35(152):1391–1417, 1980.
- The Lucas part of the BPSW test
-
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
-
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)$:
| Rule | Formula |
|---|---|
| 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):
- Constant
Num(c): returnNum(0). - Variable
Var(v): if $v = \text{var}$, returnNum(1); otherwise returnNum(0). - Sum
Add(a₁, a₂, …, aₙ): differentiate term by term, returningAdd(diff(a₁), diff(a₂), …, diff(aₙ)). - 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.
-
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)$
-
Function call
Fun(name, [u]): look up $f'(u)$ in the table, then multiply by $u'$ (chain rule). -
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):
- Let
current = expr,sum = 0. - For $n = 0, 1, \dots, \text{order}$:
- Compute $f^{(n)}(\text{point})$: replace
varincurrentbypoint(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).
- Compute $f^{(n)}(\text{point})$: replace
- 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
Atomis a Copy handle (a pointer) into an arena, not an owning value.transformallocates new nodes on the arena to build the result, but unmodified subtrees reuse the original handles (zero copy). - This makes
substitutepurely 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 ifv == var, otherwise 0.Add(args): calldiff_rawon each argument and collect the results into a newAddnode.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 todiff_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:
- Precompute
(x - point)as the common subexpressionx_minus_p. - Loop $n = 0, \dots, \text{order}$:
substitute(ctx, current, var, point)evaluates $f^{(n)}(a)$.mul_by_factorial_inversecomputes $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.
- 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
xinsin(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):
- Direct lookup layer (
integrate_raw): direct integration rules for constants, variables, sums, and simple products/powers/functions. - Rational function layer (
rational.rs): partial fraction decomposition + Hermite reduction + the logarithmic part (logarithmic-derivative identity, completing the square, Rothstein–Trager). - Risch algorithm layer (
risch.rs): structural theorems over the differential field tower $\mathbb{Q}(x, t_1, \dots, t_n)$. - Trigonometric layer (
trig.rs): rewriting via Euler's formula into complex exponentials ($t = e^{ix}$) → retry Risch on the rewritten form →realifyback to real form. - Special functions layer (
special.rs): tables of special functions such as erf, erfi, Ei, Si, Ci, Shi, Chi, Fresnel. - 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. - 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:
- Symbolic differentiation:
diff(&ctx, f, x)→ the derivative expression $f'(x)$. - Compile the evaluator:
ExpressionEvaluator::compile(f'). - 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
- 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.
- Bronstein, M. Symbolic Integration I: Transcendental Functions. Springer, 2005. — The authoritative reference on the Risch algorithm and the design basis of oCAS's integrator.
- 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
- Polynomial Algebra — polynomial operations and factorization
- Linear Algebra — matrix operations and eigenvalues
- Symbolic Calculus — the basic rules of differentiation and integration
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:
| Equation | Order |
|---|---|
| $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:
- Write the equation as $g(y), dy = f(x), dx$
- 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:
- Integrate $\partial F/\partial x = M$ with respect to $x$: $F = \int M, dx + h(y)$
- Determine $h(y)$ from $\partial F/\partial y = N$: $h'(y) = N - \partial/\partial y \int M, dx$
- 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:
| Discriminant | Characteristic roots | General 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}$:
| Discriminant | Fundamental 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$):
| Roots | Form 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:
- 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)$$
- Solve the algebraic equation:
$$Y(s) = \frac{F(s) + a(sy_0 + y_1) + by_0}{as^2 + bs + c}$$
-
Partial fraction decomposition: decompose $Y(s)$ into a linear combination of standard transform pairs
-
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:
- Determine whether the equation is a first- or second-order linear constant-coefficient equation
- Extract the coefficients $(a, b, c)$ and the forcing term $f(x)$
laplace_transformcomputes $\mathcal{L}{f}(s)$ — supporting polynomials, exponentials, trigonometric functions, and their products- Construct $Y(s) = \dfrac{F(s) + \text{initial value terms}}{as^2 + bs + c}$
inverse_laplacedecomposes the rational function withsplit_fractionand callsinverse_distinct_roots,inverse_repeated_root, orinverse_complex_rootson each term- 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:
- Extract the entries of the matrix $A$ from the system (
extract_coeff) - Compute the characteristic polynomial and find the eigenvalues (within the integers or rationals)
- For distinct real eigenvalues:
eigenvectorfinds integer eigenvectors - For a repeated eigenvalue:
generalized_eigenvectorfinds the generalized eigenvector $(A - \lambda I)\mathbf{w} = \mathbf{v}$ - For complex eigenvalues:
solve_complex_2x2constructs the real-valued fundamental solutions - 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:
| Priority | ODEType | Detection condition |
|---|---|---|
| 1 | LinearFirst | first order; $y$ and $y'$ appear linearly, no nonlinear terms like $y^2, \sin y$, and $y$ is present |
| 2 | Bernoulli | first order; contains a nonlinear power $y^n$ ($n \geq 2$) |
| 3 | Separable | first order; the terms containing $y$ can be grouped with $y'$, the remaining terms contain only $x$ |
| 4 | Exact | first order; $M + Ny' = 0$ with $\partial M/\partial y = \partial N/\partial x$ |
| 5 | Homogeneous | first-order linear; all additive terms have the same total degree in $(x,y)$ (degree-zero homogeneity), and a term free of $y$ is present |
| 6 | LinearConstantCoeff | order $\geq 2$; linear; the coefficients of the derivatives do not contain the independent variable |
| 7 | CauchyEuler | order $\geq 2$; the coefficient of $y^{(k)}$ is $c_k x^k$ |
| 8 | ReductionOfOrder | second-order linear, not of the types above |
| 9 | PowerSeries | fallback 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:
| Variant | Meaning | Use 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 series | power series / Frobenius |
System([y1, y2]) | the individual components | $2 \times 2$ systems |
Unsolved(ode) | the original equation | all 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:
- Orders above two: only linear constant-coefficient and Cauchy–Euler types are supported. General variable-coefficient higher-order equations have no dedicated solver.
- Nonlinear ODEs: apart from Bernoulli (which linearizes), nonlinear equations usually return
Unsolved. - Laplace transform: only handles constant-coefficient equations with integer coefficients, and the forcing term must have a tabulated transform.
- 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.
- 2×2 systems: only constant coefficients are supported, and the eigenvalues must be integers or rationals.
- Verification:
verify_solutionsubstitutes the solution back into the original equation and checks that the residual is zero (used only for testing).
References
-
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).
-
Coddington, E. A. & Levinson, N. Theory of Ordinary Differential Equations, McGraw-Hill, 1955. — Rigorous treatment of existence, uniqueness, and stability.
-
Ince, E. L. Ordinary Differential Equations, Dover, 1956. — Classical reference on special functions and series methods.
-
Zill, D. G. A First Course in Differential Equations with Modeling Applications, 12th ed., Cengage, 2021. — Accessible introduction with applications.
-
Tenenbaum, M. & Pollard, H. Ordinary Differential Equations, Dover, 1985. — Comprehensive classical methods including exact equations and integrating factors.
The Risch Integration Algorithm
Prerequisites
- Polynomial Algebra — polynomial rings, factorization, resultants
- Finite Fields & Modular Arithmetic — modular inverses and the extended Euclidean algorithm
- Linear Algebra — Gaussian elimination and determinants
- Symbolic Calculus — differentiation rules and the chain rule
- Polynomial GCD & Factorization — square-free factorization and Hensel lifting
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:
- 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}$.
- 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$.
- 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):
- Collect function symbols: scan the expression tree of $f$ and collect all function applications (
log,exp). - Order them: sort by dependency — if $t_2 = \exp(\log(x) + 1)$, then $t_1 = \log(x)$ must come before $t_2$.
- 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.
- Reject non-integral powers: if $f$ contains non-integral powers such as $\sqrt{x}$ (i.e. algebraic functions), reject (return
None). - 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 inKElemKRat(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)$:
| Level | Field | Generator | Derivation |
|---|---|---|---|
| $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$.
- 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.
-
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
-
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).
-
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.
- 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)$.
- Pick a factor $v$ of maximum multiplicity ($= m \geq 2$) and write $d = u \cdot v^m$.
- 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$).
- 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}}$$
- 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):
- 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$.
- 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$.
- 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).
- Degree bound (§6.3): an upper bound $n$ on $\deg r$ (the
limited_integraterefinements are not ported — they can only lower the bound, and a missed solution becomes an honest decline, never a wrong answer). - SPDE (Rothstein's special polynomial differential equation): reduce any $a \neq 1$ equation round by round to one with constant $a$, $Dh + Bh = C$.
- Polynomial-RDE dispatch:
no_cancel_b_large(leading-term peel when no cancellation is possible); the cancellation cases go throughcancel_exp/cancel_primitive, recursing one level down into the full RDE — which is where rational solutions at lower levels get used (q = 1/tis 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):
- 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).
- Undetermined coefficients: write $q = \sum_{i=0}^{n} a_i x^i$ and substitute into the equation.
- 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:
- Determine $a_m$ from the coefficient of $t^m$ in the equation (an RDE for $a_m$ in $k_{\ell-1}$).
- Substitute and lower the degree; repeat for $a_{m-1}$.
- 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$.
- 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).
- Compute $Dq = \sum (Db_i) t^i + \sum i \cdot b_i \cdot (Du/u) \cdot t^{i-1}$.
- 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$
- Solve downwards starting from $t^{m+1}$. Each $b_i$ may require a recursive call to the integrator (for the $Db_i$ part).
- 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$):
| Priority | Type | Score | Example |
|---|---|---|---|
| 1 | Logarithmic | 0 | $\log x$, $\arctan x$ |
| 2 | Inverse trig | 1 | $\arcsin x$, $\text{arccosh}, x$ |
| 3 | Algebraic | 2 | $x^2$, $\sqrt{x}$ |
| 4 | Trigonometric | 3 | $\sin x$, $\cos x$ |
| 5 | Exponential | 4 | $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:
| Integrand | Antiderivative |
|---|---|
| $\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):
| Integrand | Antiderivative | Special 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:
- Build the tower:
build_towercollects $t_1 = \exp(x)$; the tower is $\mathbb{Q}(x) \subset \mathbb{Q}(x)(t_1)$. risch_integrate: convert the integrand into a rational function in $k_1(t_1)$: $(x+1) \cdot t_1$.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$.
- Assemble the result: $q = x \cdot t_1$, converted to an atom: $x \cdot e^x$.
- 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:
| Limitation | Reason | Fallback behavior |
|---|---|---|
limited_integrate refinements and the higher-level parametric_log_deriv are not implemented) | Refinements only lower the degree bound; a miss is an honest decline | Return 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 extensions | Return the unevaluated form |
| Algebraic functions ($\sqrt{x}$, etc.) not supported | Needs algebraic-function field extensions | Common 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 incomplete | Return 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
- 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.
- Bronstein, M. Symbolic Integration II: Transcendental and Algebraic Functions, Springer, 2004. — Extensions to algebraic and special functions. Not yet covered by oCAS.
- Geddes, K. O., Czapor, S. R., & Labahn, G. Algorithms for Computer Algebra, Kluwer, 1992. — Chapter 12 offers an alternative presentation of the Risch algorithm.
- 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.
- 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.
- 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.
- Trager, B. M. "Algebraic factoring and rational function integration." Proceedings of SYMSAC '76, 1976. — Trager's algorithm (factorization over algebraic number fields).
- 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):
- Algebraic geometry: the affine variety $V(I) \subseteq \overline{\mathbb{F}}^n$ is a finite set
- 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)
- 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:
- $\text{NF}(f, G) \in f + I$ ($\text{NF}(f, G)$ is congruent to $f$ modulo $I$)
- Every term of $\text{NF}(f, G)$ is a standard monomial
- 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:
| Method | Complexity | Description |
|---|---|---|
| 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:
- Extract all leading monomials $\text{lm}(g_1), \ldots, \text{lm}(g_t)$ from $G_{\text{src}}$
- 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)
- Let $D = |\text{Std}(I)|$ and number the standard monomials $s_1, s_2, \ldots, s_D$
- 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:
- 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
- The main loop enumerates monomials in increasing target order and exactly covers all standard monomials and the monomials producing leading terms
- The linear-relation detection guarantees that the output polynomials generate exactly the leading-term ideal
minimizeandauto_reduceguarantee 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:
| Operation | Definition | Implementation |
|---|---|---|
| $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):
- Compute the Lex Gröbner basis $G$ of $I$
- 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')$
- Factor $\tilde{p}_1$ over $\mathbb{Q}$ into pairwise distinct irreducible factors $\tilde{p}_1 = q_1 \cdot q_2 \cdots q_s$
- 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)
- 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:
- Compute the Lex GB $G$
- 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')$
- 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):
- 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)
- 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}$ - 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):
- Compute the Lex GB
- Extract each univariate polynomial $p_i(x_i)$
- 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 theDomaintrait)O2: the target monomial order- the source order of the input basis is determined by the type parameter
impl MonomialOrderofgb
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:
- find a pivot (a non-zero element) column by column and swap rows
- normalize the pivot row and eliminate that column in the remaining rows
- check consistency: if some row has all zeros on the left but a non-zero right-hand side → no solution (linearly independent)
- 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 vectorsseen_mons: the corresponding list of monomialsnew_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:
- For each generator $g_j$ of $J$, call
quotient_single_generator(the Rabinowitsch trick) quotient_single_generatorimplements:- lift the generators of $I$ into $R[w]$
- add $1 - wg$
- compute the Lex GB
- eliminate $w$ (take the polynomials not involving $w$)
- 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:
- Introduce the auxiliary variable $t$ (index 0)
- Construct the generators ${t \cdot f_i} \cup {(1-t) \cdot g_j}$
- Compute the Lex GB in the extended ring $R[t]$
- 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:
- Compute the Lex GB
- Extract the univariate polynomial $p_1(x_1)$ in $x_1$ alone and compute its square-free part
- Factor the square-free part over $\mathbb{Q}$
- Separate the branches by saturation (sequentially saturating by each other factor via
ideal_saturate) - 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 termis_prime_ideal: zero-dimensional: check irreducibility; positive-dimensional: conservatively returnfalseis_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:
- Compute the Lex GB (if the input is not already a GB)
- Check zero-dimensionality
- 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
| Scenario | Recommended method | Reason |
|---|---|---|
| Zero-dimensional + Lex GB needed | Grevlex F4 → FGLM | $O(n \cdot D^3)$ is far faster than Lex F4 |
| Zero-dimensional + Grevlex GB needed | F4 directly | no order change needed |
| Positive-dimensional + Lex GB needed | Lex F4 or reorder | FGLM does not apply |
| Elimination + zero-dimensional | FGLM → elimination | the elimination theorem guarantees correctness |
| Elimination + positive-dimensional | Lex F4 | FGLM 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
- Zero-dimensional only: the staircase of a positive-dimensional ideal is infinite, so FGLM does not apply
- The $D^3$ bottleneck: when the vector-space dimension $D$ is very large (e.g. $D > 10^4$), Gaussian elimination becomes the bottleneck
- 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
- 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.
- 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).
- W. W. Adams, P. Loustaunau. An Introduction to Gröbner Bases. AMS, 1994.
- 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.
- G. Kemper. A Course in Commutative Algebra. Springer, 2011.
- T. Becker, V. Weispfenning. Gröbner Bases: A Computational Approach to Commutative Algebra. Springer, 1993.
See also:
- Rust API: Gröbner Bases and Ideals — function signatures and complete examples
- Gröbner Basis Theory — the theoretical basis of the Buchberger/F4/F5 algorithms
- Polynomial Algebra — monomial orders and multivariate division with remainder
- Polynomial GCD & Factorization — square-free factorization and factorization algorithms
- Solvers — polynomial-system solving and ODEs
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:
- Polynomial Algebra — polynomial rings, degree, division with remainder, irreducibility
- Finite Fields & Modular Arithmetic — construction of $\mathbb{F}_p$, modular inverses, CRT
- Linear Algebra — matrix operations, determinants, resultants
- Polynomial GCD & Factorization — square-free factorization, Hensel lifting, modular GCD
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:
- The minimal polynomial is irreducible in $\mathbb{Q}[x]$.
- Every polynomial $f$ with $f(\alpha) = 0$ satisfies $m \mid f$.
- 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)$:
- 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$).
- $\mathrm{GF}(p^d) = \mathbb{F}_p[x]/(\bar{m}(x))$.
- Elements are $c_0 + c_1\alpha + \cdots + c_{d-1}\alpha^{d-1}$ with $c_i \in \mathbb{F}_p$.
- 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:
- The norm $N(f)$, as a polynomial in $x$, has degree $\deg_x(f) \cdot [K:\mathbb{Q}]$.
- 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).
- 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))$.
- 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]$:
- Find a shift: $(s, g, R) \leftarrow \text{norm_with_shift}(f)$.
- Factor the norm: $R = r_1 \cdot r_2 \cdots r_k$ (irreducible factorization in $\mathbb{Q}[x]$).
- 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]$.
- 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:
- $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.
- $p$ does not divide the leading coefficients: otherwise the mapped polynomials drop in degree.
- $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.
PartialEqcompares the coefficient vectors directly (semantic equality = structural equality, because the representative is unique).Displayoutputs 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;
invreturnsNonefor non-units.
Key methods:
| Method | Description |
|---|---|
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:
- 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). - Construct $\mathrm{GF}(p^d) = \mathbb{F}_p[\alpha]/(m \bmod p)$.
- Map $a, b$ to $\mathrm{GF}(p^d)[x]$ and compute the monic GCD.
- Merge into the CRT state.
- 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:
| Type | Base field $D$ | Minimal polynomial | Purpose |
|---|---|---|---|
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:
invreturnsNonefor non-zero zero divisors (the extended Euclidean algorithm gives $\gcd \ne 1$).gcd_anfskips such primes (themin_poly_irreducible_modcheck).
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$):
| Step | Complexity | Description |
|---|---|---|
| Norm computation | $O(dn \cdot (n + d^3))$ coefficient operations | per evaluation point: Horner evaluation $O(n)$ + resultant $O(d^3)$; $O(dn)$ points in total |
| Square-free factorization | $O(n^2)$ GCD calls | Yun's algorithm; the bottleneck is the GCD |
| Modular GCD | $O(d \cdot p \cdot \log p)$ per prime | Euclidean 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 asi,mu,d0 - a variance:
Upper(contravariant) orLower(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:
- Identification: walk the arguments of all
Funnodes in the expression and count the occurrences of every label. Labels occurring exactly 2 times are dummy indices. - Validation: in canonicalization (
canon.rs), a label occurring more than twice raisesTensorCanonError::ContractedMoreThanOnce;dummy.rsdeclares theOverContractedandBadContraction(same-variance pair) error variants, butrefresh_dummiesitself does not perform a variance check. - 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 namespacesd{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
| Type | Vertex data TgNode | Meaning |
|---|---|---|
| Head vertex | Head(hash) | a tensor function head $T$; hash is the hash of the function name |
| Slot vertex | Slot(hash) | an index slot; hash is the hash of the index label (symmetric slots uniformly use Slot(0)) |
| Scalar vertex | Scalar(hash) | a scalar factor in the product |
Edge Types
| Type | Edge data TgEdge | Direction | Meaning |
|---|---|---|---|
| Head→slot | HeadToSlot(pos, flag) | directed | from a tensor head to its pos-th slot; flag=1 visible, flag=0 hidden (symmetric) |
| Contraction | Contraction(group) | undirected | an 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:
- 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.
- Automorphism orbits — the equivalence classes of vertices under the action of the automorphism group.
- 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:
- Choose the smallest non-trivial cell $C_{\min}$.
- 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.
- Re-refine the partition after individualization.
- 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:
- Row symmetrization: fully symmetrize the indices within each row.
- Column antisymmetrization: fully antisymmetrize the indices within each column.
- 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:
-
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).
-
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):
- 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.
- For each combination $(\sigma, \tau)$, form the composed permutation $\tau \circ \sigma$; the term coefficient is the column permutation parity $\operatorname{sgn}(\tau)$.
- Each term contributes $\operatorname{sgn}(\tau) \cdot T(i_{\tau\sigma(1)}, \dots, i_{\tau\sigma(n)})$.
- 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
| Type | Field | Graph-encoding behavior | Young-projection behavior |
|---|---|---|---|
| Symmetric subsets | symmetric_subsets | slots are marked hidden (is_slot_hidden = true); the graph isomorphism may permute them freely | preserves the corresponding row symmetry |
| Antisymmetric subsets | antisymmetric_subsets | slots 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 permutations | cyclic | slots are marked hidden | symmetrization 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):
- If the expression is a sum (
Add), canonicalize each term and verify that the free indices are consistent. - 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_hiddenis 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 withg.canonize(), and rebuild the expression withreconstruct.
- Fast path: if the tensor has symmetric subsets, no antisymmetric subsets, and all slots are hidden (
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 useSlot(hash(label)). - add the directed edge
Head → Slot.
- create a head vertex
- 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_posor label order), determine the slot labels:- contracted slots: assign the canonical dummy names
d0,d1, … - free slots: keep the original labels.
- contracted slots: assign the canonical dummy names
- 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 andcell_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, containingvertex_map(original → canonical mapping),orbits(automorphism orbits),automorphism_group_size(group order), andgraph(the canonical graph).
Key implementation details:
- Refinement (
Partition::refine): uses thestable_belowpointer optimization — cells that are already stable no longer participate in split checks. When a cell is split,stable_belowretreats 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.directiondistinguishes 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):
- Collect the occurrence counts of all
Funnode arguments in the expression. - Identify the dummy indices (occurring exactly twice).
- For each dummy index, query its index group (
registry.index_group) and assign a number within the group. - Rename: dummy indices of group 0 are named
d0,d1, …; those of group $g$ are namedd{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
Hparameter) 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
- 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.
- McKay, B. D. Practical Graph Isomorphism. Congressus Numerantium, 30:45–87, 1981 (updated 2014). — The original paper on the nauty algorithm.
- 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.
- Cox, D., Little, J., O'Shea, D. Ideals, Varieties, and Algorithms. Springer, 4th ed., 2015. — Polynomial-algebra foundations; reference for Gröbner basis theory.
- 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:
| Concept | Description |
|---|---|
| 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 numbers | the sample mean converges to the expectation as the sample size grows |
| Central limit theorem | the 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:
- Unbiasedness: $\mathbb{E}[\hat{I}] = I$ (its expectation equals the true value exactly).
- Variance: $\text{Var}(\hat{I}) = \frac{V^2}{N} \text{Var}(f(\mathbf{X}))$.
- 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)$:
- Pick a bin: for the $k$-th dimension, choose the bin number $j_k$ uniformly at random from ${0, 1, \dots, M-1}$.
- Pick a point: choose $x_k$ uniformly at random within $[b_{j_k}, b_{j_k+1}]$.
- 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})$$
- 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 estimateerror: the current combined standard errorchi_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
| Parameter | Default | Meaning |
|---|---|---|
n_bins | 64 | number of bins per dimension $M$ |
n_samples | 10,000 | number of samples per iteration $N$ |
iterations | 10 | number of adaptive iterations $R$ |
learning_rate | 1.5 | grid-update damping parameter $\alpha$ |
seed | 0x0C45 | deterministic 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 thanthread_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_traininglocates the bin by binary search ($O(\log M)$) and accumulatesweight × 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, socum[j+1] - cum[j]can still be zero (a bin completely flat), in which casefrac = 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
| Scenario | Recommended method |
|---|---|
| The integrand has an elementary antiderivative | Symbolic 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 layers | Vegas + increase n_bins and iterations |
| Differentiable integral values are needed | Automatic differentiation (see Automatic Differentiation) |
Limitations of Vegas
-
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.
-
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.
-
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.
-
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
-
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.
-
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.
-
Lepage, G. P. "Vegas: An adaptive multi-dimensional integration program." Cornell preprint CLNS 80-447, 1980. — Implementation details and usage guide for Vegas.
-
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.
-
Neal, R. M. "Annealed importance sampling." Statistics and Computing, 11(2):125–139, 2001. — The theoretical foundations of importance sampling.
-
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:
| Variant | Behavior |
|---|---|
Auto | currently routes to F4 (the F5 switch point will be calibrated against cyclic-n benchmarks in the future) |
Buchberger | classical S-polynomial iteration |
F4 | matrix batch reduction |
F5 | signature criterion |
All algorithms output a reduced Gröbner basis (GroebnerBasis::minimize().auto_reduce()).
The Buchberger Algorithm
GroebnerBasis::buchberger implements the classical Buchberger algorithm:
- Initialize the basis with the input polynomial set
- Construct all critical pairs (S-polynomials)
- For each pair, compute the S-polynomial and reduce it by multi-step division against the current basis
- If the remainder is nonzero, add it to the basis and update the critical pairs
- Repeat until no new polynomials are added
- Finally call
minimize()(remove redundancy) andauto_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:
- Add the two components of the S-polynomial pair to the matrix
- 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
- 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_fpperforms 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:
minimize(): remove redundant elements whose leading terms are divisible by the leading term of another basis elementauto_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 varietyhilbert_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):
- Compute a Gröbner basis GB of the generators
- Reduce $f$ against GB
- 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:
- Introduce a new variable $w$ in the extended ring $k[x_1, \dots, x_n, w]$
- Compute $\text{GB}(I' \cup {1 - wg'})$
- Eliminate $w$ (take the elements of the Lex GB not containing $w$)
- 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:
- Introduce an auxiliary variable $t$ (index 0)
- Construct $t \cdot f_i$ and $(1-t) \cdot g_j$
- Compute the Lex GB
- 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:
- Take the Lex GB (which naturally has triangular form)
- 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}$, …
- Real roots are computed by Sturm root isolation (at
f64precision)
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):
- Compute the partial derivatives $\partial f_i / \partial x_j$ of all generators with respect to all variables
- Take the GCD of all nonzero partial derivatives as $h$
- $\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:
- Take the irreducible factorization of the univariate polynomials in the Lex GB
- Each irreducible factor $p_i^{e_i}$ corresponds to one primary component
- Separate the components by saturation: $Q_i = I : \prod_{j \neq i} p_j^\infty$
- The associated prime ideal is $\mathfrak{p}_i = \langle p_i \rangle$
Returns Vec<PrimaryComponent>, each containing:
primary: the GB of the primary idealprime: 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 returnsfalsefor positive-dimensional idealsis_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+):
| Order | Description |
|---|---|
Lex | lexicographic: $x_1 > x_2 > \cdots$, suitable for elimination |
Grlex | graded lexicographic: total degree first, then lexicographic |
Grevlex | graded reverse lexicographic: total degree first, then reverse lexicographic |
WeightOrder | weighted order: descending by $\sum w_i e_i$ |
BlockOrder | block elimination: variables grouped into blocks, each with an independent sub-order |
MatrixOrder | general 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
| Scenario | Recommended strategy |
|---|---|
| zero-dimensional + Lex needed | compute with Grevlex first, then convert to Lex with fglm |
| general ideal + different order needed | reorder (re-run F4 in the new order) |
| elimination | use Lex directly or MatrixOrder::elimination_order |
Benchmark Data
Criterion timings (cyclic systems, on this machine):
| System | Buchberger | F4 | Speedup |
|---|---|---|---|
| cyclic-3 ℚ | 308 µs | 147 µs | 2.1× |
| cyclic-4 ℚ | 3.99 ms | 2.13 ms | 1.9× |
| cyclic-3 ℤ₁₃ | 582 µs | 276 µs | 2.1× |
| cyclic-4 ℤ₁₃ | 6.19 ms | 2.80 ms | 2.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
SparseMultivariatePolynomialand the monomial-order traits - Coefficient domains: Coefficient Domains — the
Domaintrait 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:
| Domain | Polynomials | Algorithms |
|---|---|---|
| $\mathbb{Z}[x]$ | Univariate | Square-free factorization, Berlekamp–Zassenhaus, Hensel lifting |
| $\mathbb{F}_p[x]$ | Univariate | Square-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]$ | Multivariate | Wang 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]$ | Multivariate | EEZ Hensel lifting (with characteristic-$p$ $p$-th power handling; non-constant LC via field Wang preprocessing) |
| $\mathbb{Q}(\alpha)[x]$ | Univariate | Trager'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:
- Compute the content and reduce to a primitive polynomial.
- Compute the square-free decomposition.
- Choose a small prime $p$ such that the reduction stays square-free and has the same degree as the input.
- Factor modulo $p$ using Cantor–Zassenhaus (which internally uses the Berlekamp matrix method for small primes $p \le 1000$).
- 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:
- Choose an evaluation point $y = \alpha$ so that the univariate image $f(x, \alpha)$ is square-free and has the fewest irreducible factors.
- Factor the univariate image over $\mathbb{Z}[x]$.
- 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:
- 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.
- 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.
- 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$.
- 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.
- 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:
- Square-free factorization (Yun) over $K$, using the modular GCD below instead of a pseudo-remainder sequence.
- 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).
- Rational factorization: the square-free norm is factored over $\mathbb{Z}$ (Hensel path).
- 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.
- 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) andeez_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 wholeu64range.
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 ofbasemodulo the primepand 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:
- Fast table — inline structural rules: power rules (
x^n,(ax+b)^n, including fractional exponents),sin/cos/expof linear arguments, and direct table entries such aslog(x). Fast and always attempted first. - Rational-function integrator — Hermite reduction plus the
logarithmic part (logarithmic-derivative identity, completing the
square, Rothstein–Trager). Handles every rational function of
x. - 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).
- Risch algorithm — elementary transcendental towers built from
logandexp(the tower recursion is capped byMAX_RISCH_DEPTH = 16). - Trigonometric rewrite —
sin/cos/tan/… rewritten intoexp(I·x)and re-integrated by Risch, then converted back to real form on a best-effort basis. - Special-function table — non-elementary integrals with closed
forms in terms of
erf,Ei,Si,Ci, FresnelS/C, … - 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 withIntegrateOptions { rules: false }. - Trig product-to-sum / power reduction (0.27.x) — products and
powers of
sin/cosat linear arguments reduce to multiple-angle sums, integrated termwise. - Heuristic techniques — integration by parts, trigonometric substitution, Weierstrass $t = \tan(x/2)$, Euler substitutions.
- Bounded expansion retry (0.27.x) — products are distributed over sums (64-term budget) and integrated termwise.
- 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
loglevels and by the Risch differential equationDq + f·q = gatexplevels — 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 dxhas no elementary antiderivative — it is answered by the special-function table asEi(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):
| Integrand | Result |
|---|---|
exp(-x²) | (√π/2)·erf(x) |
exp(x²) | (√π/2)·erfi(x) |
exp(c·x²), c < 0 | √π/(2√(-c))·erf(√(-c)·x) |
exp(x)/x | Ei(x) |
sin(x)/x | Si(x) |
cos(x)/x | Ci(x) |
sinh(x)/x | Shi(x) |
cosh(x)/x | Chi(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:
| Value | Meaning |
|---|---|
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:
- The GCD of numerator and denominator is divided out.
- The leading coefficient of the denominator is normalized (positive for ordered domains, 1 for finite fields).
Arithmetic
All standard operations are supported:
| Operation | Method | Strategy |
|---|---|---|
| Addition | a.add(&b) | Cross-multiply, then canonicalize |
| Subtraction | a.sub(&b) | Via negation + addition |
| Multiplication | a.mul(&b) | Cross-cancel GCDs, then multiply |
| Division | a.div(&b) | Via b.inv() + multiplication; returns None when the numerator of b is zero |
| Negation | a.neg() | Negate numerator |
| Inverse | a.inv() | Swap numerator/denominator; returns None when the numerator is zero |
| Power | a.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:
- Polynomial division: If $\deg(p) \geq \deg(q)$, extract the polynomial
part via
div_rem. - Square-free factorization: Decompose the denominator into square-free factors $f_i^{e_i}$.
- Diophantine CRT: For multiple factors, solve the polynomial Chinese Remainder Theorem to split the fraction.
- 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:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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
| SymPy | oCAS |
|---|---|
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
EuclideanDomainusing square-free factorization. Full factorization into irreducibles requiresIntegerDomainorFiniteField. - Multivariate partial fractions (
apart_multivariate) are deferred to a future version. - FFT/NTT multiplication is available since 0.12.1 via the
nttfeature.
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:
- Normalization (
normalize_ode): algebraically simplifies and standardizes the input equation - Classification (
classify_ode): returns all applicable solving methods in priority order - 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:
| Variant | Meaning | Typical 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 solution | power series, Frobenius |
System(&[Atom]) | system solution components $(y_1, y_2)$ | $2\times2$ systems |
Unsolved(ode) | could not be solved symbolically | when 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):
- Check
is_first_order_linear: $y$ and $y'$ appear only to the first power →LinearFirst - Check
is_bernoulli: some $y^n$ ($n \geq 2$) plus a linear $y$ term →Bernoulli - Check
is_separable: additive terms without $y$ plus additive terms with $y$ →Separable - Check
is_exact: in $M + Ny' = 0$, $\partial M/\partial y = \partial N/\partial x$ →Exact - Check
is_homogeneous: all terms have the same total $(x, y)$ degree →Homogeneous
Second-order and higher linear ODEs:
is_constant_coeff_linear: the coefficients of $y, y', y''$ do not contain $x$ →LinearConstantCoeffis_cauchy_euler: the terms are of the form $c_k x^k y^{(k)}$ →CauchyEuler- Second-order linear →
ReductionOfOrder(tries simple candidate solutions)
Global fallback:
- Linear ODEs →
PowerSeries(power series or Frobenius)
Exactness Detection
The exact-equation test is_exact works as follows:
- Split the equation into $M(x, y) + N(x, y),y' = 0$ with
split_mn - Replace $y(x)$ by the bare symbol $y$ so that $M, N$ become bivariate expressions
- Compute $\partial M/\partial y$ and $\partial N/\partial x$
- First compare the normalized strings (
to_string()afternormalize) - 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:
- Split the equation into $y$-terms and non-$y$ ($x$) terms with
separate_by_func - Replace $x$ by $y$ in the $y$-terms (
substitute_var) - Integrate both sides: $\int g(y),dy$ and $\int f(x),dx$
- 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):
- Extract $p(x)$ and $q(x)$ with
extract_linear_coeffs - Compute the integrating factor $\mu(x) = e^{\int p,dx}$
- 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:
- Find $n$ in $y^n$ recursively with
find_bernoulli_power - Substitute $v = y^{1-n}$; the equation becomes $v' + (1-n),p(x),v = (1-n),q(x)$
- Solve for $v$ with the linear-equation method
- 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:
- Split into $M$ and $N$ with
split_mn($y'$ must appear linearly, otherwiseNoneis returned) - Check exactness (
partials_equal) - If not exact, try an integrating factor (
find_integrating_factor) - Integrate $M$ with respect to $x$ to obtain part of the potential $F$
- Compute the correction term $g(y) = N - \partial F/\partial y$
- 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:
- Substitute $v = y/x$ (i.e. $y = vx$), obtaining $y' = v + xv'$
- The equation becomes $v + xv' = f(v)$, i.e. $xv' = f(v) - v$ (separable in $v$ and $x$)
- Split into $v$-terms and $x$-terms with
separate_by_var - 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:
- Extract $a, b, c$ and $f(x)$ with
extract_second_order_coeffs - Verify that $a, b, c$ do not contain $x$ (
contains_x) - Compute the discriminant $\Delta = b^2 - 4ac$ of the characteristic equation $ar^2 + br + c = 0$
- Construct the homogeneous solution $y_c$
- Construct a particular solution $y_p$ (undetermined coefficients or variation of parameters)
- Return $y = y_c + y_p$
Three Cases for the Homogeneous Solution
constant_coeff_basis constructs the fundamental solution set from the discriminant:
| Discriminant | Fundamental 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 type | Detection | Solving strategy |
|---|---|---|
| Polynomial $f(x)$ | is_polynomial_in | set $y_p = \sum A_k x^{k+s}$, substitute back, solve for the coefficients |
| Exponential $F e^{kx}$ | extract_exp_forcing | set $y_p = A x^s e^{kx}$ |
| Trigonometric $f_c\cos\omega x + f_s\sin\omega x$ | extract_trig_forcing | set $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$:
- Compute the Wronskian $W = y_1 y_2' - y_1' y_2$
- $u_1' = -y_2 g / W$, $u_2' = y_1 g / W$
- Integrate to obtain $u_1, u_2$
- $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:
- Extract the coefficients with
extract_cauchy_euler_coeffs - Reduce to constant coefficients (divide by the powers of $x$)
- Solve the indicial equation $ar(r-1) + br + c = 0$, i.e. $ar^2 + (b-a)r + c = 0$
- Construct the homogeneous solution from the discriminant:
| Discriminant | Fundamental 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)$ |
- 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:
- Try candidate solutions $y_1 \in {1, x, x^2, e^x, e^{-x}, e^{2x}}$
- Verify $a y_1'' + b y_1' + c y_1 = 0$ (
satisfies_extracted) - 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$
- The particular solution is obtained by variation of parameters
- 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:
| Kernel | Laplace 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):
- Construct symbolic coefficients $a_0, a_1, \ldots, a_{N-1}$
- Manually construct the series $S = \sum a_n (x - x_0)^n$ together with its first and second derivatives $S', S''$ (
build_series_triple—diffis not used, to avoid differentiating the $a_n$) - Substitute into the ODE: $R(x) = a,S'' + b,S' + c,S - f = 0$
- Satisfy order by order: for $k = 0, 1, \ldots$, compute $R^{(k)}(x_0) = 0$
- Each condition determines one new coefficient $a_{k+\text{order}}$ linearly
- Solved coefficients are substituted back immediately
Key implementation details:
substitute_seriesrecursively replaces all $y(x)$, $y'(x)$, $y''(x)$ in the equation by the series formssolve_linear_coeffsolves $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
Noneand 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):
- Extract the coefficients $a(x), b(x), c(x)$ and verify $f = 0$ (homogeneous equations only)
- Set $y = x^r \sum a_n x^n$ and construct $y, y', y''$
- Substitute into the ODE and group the residual by powers of $x$
- The lowest-power group yields the indicial equation $Ar^2 + Br + C = 0$
- Find rational roots of the indicial equation (
indicial_coeffs→isqrt+ discriminant) - 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.
eigenvectorsolves $(A - \lambda I)\mathbf{v} = 0$: if $a_{12} \neq 0$, take $\mathbf{v} = (a_{12},; \lambda - a_{11})$- 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:
- Find the eigenvector $\mathbf{v}$ (
eigenvector) - Find a generalized eigenvector $\mathbf{w}$ satisfying $(A - \lambda I)\mathbf{w} = \mathbf{v}$ (
generalized_eigenvector) - 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).
- $\alpha = \text{tr}/2$, $\beta = \sqrt{-\Delta}/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)$
- 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_degreecounts the occurrences offunc/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:
- Decompose additive terms into a rational coefficient plus base-exponent factor pairs
- Group by base-exponent signature
- Sum the rational coefficients within each group
- 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
expform
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
| File | Responsibility |
|---|---|
ocas-calc/src/ode/mod.rs | Entry points: dsolve, dsolve_ivp, dsolve_system, ODE, ODESolution |
ocas-calc/src/ode/classify.rs | Classifier: classify_ode, ODEType, the detection functions |
ocas-calc/src/ode/first_order.rs | First-order solvers: separable, linear, Bernoulli, exact, homogeneous |
ocas-calc/src/ode/second_order.rs | Second-order solvers: constant coefficient, Cauchy–Euler, reduction of order, variation of parameters |
ocas-calc/src/ode/laplace.rs | Laplace transform: forward transform table, inverse transform (three root types), IVP solving |
ocas-calc/src/ode/series.rs | Power 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.rs | Utilities: order, linearity, like-term collection, exponential simplification, solution substitution |
See Also
- ODE Solving Theory — mathematical derivations and proofs of each method
- Symbolic Calculus — theoretical foundations of differentiation and Taylor expansion
- Rust API: Solvers — API reference for
dsolve,dsolve_ivp,dsolve_system - Rust API: Calculus — API reference for
diff,integrate
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:
| Operation | Function | Description |
|---|---|---|
| Membership | ideal_contains(gens, f, algo) | Test if $f \in I$ |
| Sum | ideal_sum(I, J) | $I + J$ |
| Product | ideal_product(I, J) | $I \cdot J$ |
| Quotient | ideal_quotient(I, J) | $I : J$ |
| Saturation | ideal_saturate(I, J) | $I : J^\infty$ |
| Intersection | ideal_intersection(I, J) | $I \cap J$ |
| Elimination | eliminate(gens, elim_vars, algo) (ocas_poly::groebner) | Eliminate variables |
| Radical | ideal_radical(gens) | $\sqrt{I}$ |
| Primary decomposition | primary_decomposition(gens) | Decompose into primary components |
| Hilbert series | hilbert_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
| Type | Form | Method |
|---|---|---|
| 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 factors | non-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 order | second-order linear | $y_2 = y_1\int e^{-\int p}/y_1^2$ |
| Variation of parameters | second-order non-homogeneous | Wronskian formula |
| Undetermined coefficients | polynomial/exponential/trigonometric forcing | Coefficient matching + resonance |
| Power series | ordinary point | Coefficient recursion |
| Frobenius | regular singular point | Indicial equation + recursion |
| Laplace IVP | first/second-order linear IVP | dsolve_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:
| Error | Meaning |
|---|---|
EmptySystem | No equations provided |
NonLinear | System is not linear in the requested variables |
NonSquare | Number of equations ≠ number of unknowns |
Inconsistent | No solution exists |
Underdetermined { rank } | Infinitely many solutions |
ResultNotInDomain | Solution contains fractions when integers are required |
See also
- Rust API — domain types and polynomial operations
- Rewrite & Simplification — simplifying solved expressions
- Performance — Gröbner basis benchmark results
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:
| Level | Name suffix | Matches |
|---|---|---|
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 matchInconsistentBinding— same wildcard name bound to different valuesBudgetExhausted— 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 theeggfeature) - 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
- Rust API — building expressions and patterns from Rust
- Evaluation & JIT — numeric evaluation after simplification
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_ + __restmatches any sum with at least one captured term. - Multiple wildcards in AC:
x_ + y_ + z_againsta + b + c + dcorrectly 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
| Path | Feature | Latency | Throughput | Best for |
|---|---|---|---|---|
| Interpreter | (default) | Low (no compile overhead) | Medium | Single-shot, interactive use |
| JIT | jit | High (compile) + Low (run) | High | Repeated evaluation of same expression |
| SIMD | simd | Low–Medium | Very 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 — Monte Carlo integration (Vegas)
- Rust API — building expressions for evaluation
- Performance — benchmark results across all three paths
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 point | Description |
|---|---|
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
| Field | Default | Effect |
|---|---|---|
n_bins | 64 | Number of grid bins per dimension |
n_samples | 10,000 | Samples per iteration |
iterations | 10 | Number of adaptive iterations |
learning_rate | 1.5 | Grid adaptation speed (1.0–2.0) |
seed | 0x0C45 (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 → f64function; 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).
| Type | Role |
|---|---|
DualShape | Layout descriptor: variable groupings, multiplication table |
HyperDual<T> | Concrete dual number with value + derivative slots |
DualCoeff | Trait: 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.
| Operation | Supported? |
|---|---|
+, -, *, / | Yes |
inv() (multiplicative inverse) | Yes |
pow / exp / log / trig | No (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
Rationalcoefficients 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 onHyperDual. - 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:
| Property | Type | Meaning |
|---|---|---|
label | Atom | Index name (typically a Greek letter or i, j, …) |
position | IndexPosition | Upper (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
contractmanually. - The
TensorSymmetrymetadata supports onlySymmetricandAntisymmetric; finer-grained symmetries (subset symmetriessymmetric_subsets, cycliccyclic) are expressed through the 0.22.0SymmetrySpec/TensorRegistryin graph canonicalization. - Graph canonicalization requires registering each tensor's symmetry
spec in a
TensorRegistryfirst; 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
- 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.
- Run McKay refinement-individualisation (1-WL colour refinement + individualisation DFS with automorphism-orbit pruning) on the graph.
- 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, ®).unwrap(); println!("canonical: {}", ct.canonical_form); // T(i,d0) * U(d0,k) }
Symmetry Specification
| Spec | Meaning |
|---|---|
SymmetrySpec::none() | All slots are independent |
SymmetrySpec::fully_symmetric(n) | Slots interchangeable (hidden positions) |
SymmetrySpec::fully_antisymmetric(n) | Slots antisymmetric (Ricci identity verification) |
| Custom subsets | symmetric_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, ®)?; // "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
| Benchmark | Covers |
|---|---|
arena | Arena allocation throughput |
parse / normalize | Expression parsing and normalization |
poly_dense / poly_sparse | Polynomial arithmetic |
poly_gcd | Univariate and bivariate polynomial GCD |
poly_factor | Square-free and full factorization (ℤ/𝔽_p/multivariate/ANF) |
groebner | Gröbner bases (cyclic-n ideals) |
calculus / rewrite | Differentiation, Taylor, rule-based simplification |
integrate | Symbolic integration (rational/Risch/special functions) |
eval_interpreter / eval_jit / eval_simd | Numeric evaluation paths |
eval_streaming | Streaming evaluation (constant memory) |
eval_estrin / eval_pulp | Fast polynomial evaluation (Estrin / pulp SIMD) |
ntt_poly_mul | NTT polynomial multiplication |
gmp_integer | GMP integer arithmetic |
sympy_comparison | Head-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):
| Workload | Interpreter | JIT | Speedup |
|---|---|---|---|
| Polynomial (single output) | 221 µs | 2.27 µs | 97× |
| Trig 3-output | 479 µs | 22.4 µs | 21× |
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:
| Workload | Per-row evaluate | StreamingEvaluator | Speedup |
|---|---|---|---|
| 100k rows, polynomial | 23.4 ms | 16.8 ms | 28% |
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 example | oCAS benchmark | Workload |
|---|---|---|
polynomial_gcd | poly_gcd | Integer & rational polynomial GCD |
factorization | poly_factor | x^n - 1 square-free / full factorization |
groebner_basis | groebner | cyclic-4 ideal |
derivative | calculus | Symbolic differentiation |
series | calculus | Taylor 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:
- Generates an input (expression, polynomial, system of equations, etc.)
- Computes a result using oCAS
- Computes the equivalent result using a reference system
- Asserts that both results are semantically equal
The modules cover the full breadth of oCAS functionality:
| Module | Tests | Covers |
|---|---|---|
calculus | 16 | Differentiation, Taylor, integration (SymPy-checked) |
evaluation | 6 | Numeric evaluation |
finite_field | 5 | Finite field arithmetic |
groebner | 24 | Gröbner basis computation |
integral_risch | 15 | Risch symbolic integration |
integral_rules | 1 | 100-case rule-table sample cross-checked against SymPy |
integral_verify | 3 | Numerical 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_solve | 5 | Linear solvers |
matrix | 5 | Matrix operations |
normalize | 8 | Expression normalization |
ntheory | 11 | Number theory (cross-checked against SymPy ntheory) |
ode | 34 | ODE solving |
parse | 12 | Expression parsing and printing (incl. unary minus / bare subtraction) |
partial_fraction | 8 | Partial fraction decomposition |
poly_arithmetic | 6 | Dense/sparse polynomial arithmetic |
poly_factor | 12 | Square-free and full factorization |
poly_factor_anf | 21 | Algebraic number field factorization |
poly_gcd | 5 | Polynomial GCD |
resultant | 8 | Resultants |
rewrite | 8 | Rewriting and simplification |
root_isolation | 4 | Real 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
| Issue | Module | Status |
|---|---|---|
sin(x)^2 + cos(x)^2 → 1 requires egg feature | rewrite | Simplifies with the egg feature |
| Bernoulli forcing y^n confuses linear coefficient extraction | ode | Known limitation (#[ignore]) |
| Integrator missing tan/sec table entries | ode | Planned (#[ignore]) |
See also
- Performance — benchmark suite details
- Rust API — core types used in tests
- Contributing — how to add new correctness tests
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
| Feature | Crate | Backend | License |
|---|---|---|---|
gmp | ocas-core, ocas-domain, ocas-poly | GMP integers/rationals via rug | LGPL |
mpfr | ocas-domain | MPFR real balls via rug | LGPL |
flint | ocas-poly | FLINT 3 (Linux/WSL only) | LGPL |
ntt | ocas-poly | Number-theoretic transform for polynomial multiplication (self-implemented) | LGPL |
sprs | ocas-poly | Sparse matrix backend (via sprs crate) | MIT |
fast-poly | ocas-eval | Fast polynomial evaluation (SIMD-friendly) | MIT |
jit | ocas-eval | Cranelift JIT compilation | Apache-2.0 |
simd | ocas-eval | SIMD-vectorized evaluation | MIT |
egg | ocas-rewrite | E-graph equality saturation | MIT |
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
flintfeature is not supported on Windows due to upstreamflint3-syslimitations (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 --allbefore submitting. - Prefer arena allocation for expression trees; avoid per-node
Box/Rc. - Use
thiserrorfor 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/flintthat require system C libraries are excluded). For the full backend API docs, build locally withcargo 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.