Symbolica 3.0: exact solving and numerical precision

Changes since Symbolica 2.2, including exact equation solving, algebraic numbers, Python numerical scalars, and Numerica 3.0.
symbolica
release
rust
python
numerica
Author

Ben Ruijl

Published

September 12, 2026

Symbolica 3.0 expands exact equation solving and algebraic-number support, introduces precision-aware Python numerical scalars, and improves polynomial arithmetic and numerical evaluation. This overview covers changes since Symbolica 2.2, including the bundled Numerica 3.0 update.

For changes to existing code, see the migration guide.

Exact algebra and solving

  • Added exact polynomial-system solving with parameters and solution families. Expression.solve() returns a SolutionSet containing variable assignments, free variables, validity conditions, and complete or generic coverage. Solving supports complex, real, rational, and integer domains; unsupported problems raise an error. The old solve_linear_system() entry point was removed.
  • Added canonical algebraic roots, certified root embeddings, and algebraic contexts for combining generators. Algebraic coefficients can be retained through polynomial conversion and factorization, including factorization in a specified algebraic extension.
  • Extended partial fractions to multiple variables with expr.apart(x, y) and automatic variable selection with expr.apart().
  • Improved polynomial multiplication, homogeneous multiplication, modular GCD reconstruction, factor reconstruction, resultants, and Gröbner-basis reduction. Polynomial storage is more compact, and coefficient domains can supply specialized arithmetic kernels.
  • Unified checked polynomial division and improved failure handling for non-exact quotients. Fixed rational-polynomial constants, variable contexts, and arithmetic edge cases; derivatives cancel common factors, and powers use binary exponentiation.

Nonlinear systems with parameters

Solve the coupled system

\[ x^2 + y^2 = a, \qquad xy = b \]

for \(x\) and \(y\), keeping \(a\) and \(b\) as symbolic parameters. Symbols omitted from the unknowns list are treated as parameters automatically:

from symbolica import Expression, S

x, y, a, b = S("x", "y", "a", "b")
solutions = Expression.solve([
    (x**2 + y**2).eq(a),
    (x*y).eq(b),
], [x, y])

The result contains four exact solution branches:

\[ \begin{aligned} t_k &= \operatorname{root}(t^4-at^2+b^2,\,k), && k=0,1,2,3, \\ (x,y) &= \left(\frac{at_k-t_k^3}{b},\;t_k\right). \end{aligned} \]

The new root object keeps the answer compact and exact, even with symbolic parameters. Symbolica also returns the conditions under which these branches cover all solutions. Here they simplify to

\[ b\ne 0, \qquad a^2\ne 4b^2. \]

Substituting parameter values also simplifies the roots. For \(a=5\) and \(b=2\), the four branches become

\[ \{(1,2),\;(2,1),\;(-1,-2),\;(-2,-1)\}. \]

Exceptional parameter values can have solutions too. Solving the original system at \(a=5\), \(b=0\) gives

\[ \{(\sqrt{5},0),\;(-\sqrt{5},0),\;(0,\sqrt{5}),\;(0,-\sqrt{5})\}. \]

Solutions are complex by default. Pass domain=Reals to request real solutions. The migration guide explains how to work with solution branches and their validity conditions.

Algebraic roots

The new root object represents an exact selected root of a polynomial. It can simplify automatically, appear in symbolic expressions, and be evaluated at arbitrary precision. In Python, polynomial.root(index, variable=x) selects a root by its zero-based index in Symbolica’s canonical ordering.

Automatic simplification

Root construction simplifies the defining polynomial and returns a simpler expression when possible. For example, the roots of \((x-3)(x^2-2)\) become radicals or integers immediately:

from symbolica import S

x = S("x")
polynomial = (x - 3) * (x**2 - 2)
roots = [polynomial.root(k) for k in range(3)]

This immediately produces

\[ \left[-\sqrt{2},\;\sqrt{2},\;3\right]. \]

When a root remains in polynomial form, it is still an exact algebraic number and can be evaluated at the requested precision:

alpha = (x**3 - x - 1).root(0)
value = alpha.evaluate({}, decimal_digit_precision=80)

The exact result and its numerical value, shown here to ten decimal places, are

\[ \begin{aligned} \alpha &= \operatorname{root}(\xi^3-\xi-1,\,0), \\ \alpha &\approx -0.6623589786 - 0.5622795121\,i. \end{aligned} \]

The canonical variable \(\xi\) is bound inside the root object; no value for it is needed during evaluation.

Algebraic-number coefficients

The defining polynomial can itself contain algebraic numbers. For example:

from symbolica import N, S

x = S("x")
sqrt2 = N(2).sqrt()
polynomial = x**2 - (1 + sqrt2)*x + sqrt2
roots = [polynomial.root(k, variable=x) for k in range(2)]

Symbolica simplifies the roots exactly:

\[ x^2-(1+\sqrt{2})x+\sqrt{2}=0 \qquad\Longrightarrow\qquad \left[1,\;\sqrt{2}\right]. \]

More general roots can be converted to a defining polynomial over the rationals:

beta = (x**3 - sqrt2*x - 1).root(2, variable=x)

The result is

\[ \begin{aligned} \beta &= \operatorname{root}(\xi^6-2\xi^3-2\xi^2+1,\,5) \\ &\approx 1.4504054433. \end{aligned} \]

It is the same selected root of \(x^3-\sqrt{2}x-1\). Symbolica preserves that choice while changing the defining polynomial, so the root index can change during normalization.

Expressions and pattern matching

  • Python == and != now return Booleans. Use .eq() and .ne() to construct deferred conditions; ordered comparisons also construct conditions. Unknown conditions evaluate to None.
  • Mathematical property queries distinguish a proven false result from an unknown one. Python’s is_real(), is_integer(), is_scalar(), is_positive(), and is_nonnegative() return True, False, or None.
  • Match callbacks use True, False, and None for accept, reject, and undecidable. Matching levels now use separate min_level and max_level bounds in place of level_range.
  • Added the Flat function attribute (is_flat=True in Python), which flattens nested calls to the same function, and lookup of existing symbols through Expression.get() in Python and Symbol::get() in Rust.
  • Extended expression indexing and pruned impossible pattern matches earlier.
  • Series coefficient access distinguishes known zero coefficients from terms outside the computed order.
  • Added Python access to further transcendental functions, including reciprocal trigonometric and hyperbolic functions and their inverses.
  • Improved implicit multiplication before parentheses, Mathematica built-in parsing, fraction formatting, and canonical strings. Canonical strings bypass custom printers.
  • Expression export includes only the state needed by the expression. Import preserves symbol metadata and variable mappings more reliably, and tensor sums retain completed contractions.

Breaking change: antisymmetric function matching

Rules that rely on permuting the arguments of an antisymmetric function must be updated. In 2.2, the matcher considered argument permutations. In 3.0, antisymmetric functions match argument by argument in the order stored in the expression, just like ordinary functions.

Antisymmetry still applies during normalization: f(2, 1) becomes -f(1, 2), and f(1, 1) becomes zero. However, calls containing wildcards retain their argument order, so the position of each wildcard in a pattern now matters. For example, f(w_, 1) does not match f(1, 2); use f(1, w_) to match that stored order.

Review replacements, match iterators, and restrictions that depend on a particular wildcard receiving an argument. If a rule needs several argument orders, express them explicitly and account for the permutation sign in the replacement. See the migration example.

Numerical evaluation

  • Exposed Numerica’s immutable Float and ComplexFloat in Python. Arbitrary- precision evaluation, numerical solving, and high-precision polynomial roots return these scalars. Decimal inputs remain supported.
  • Added precision-aware arithmetic, elementary functions, constants, formatting, Decimal conversion, and random sampling on the new scalars.
  • Added non-inlined sub-evaluators, configurable through FunctionDefinition(..., inlining="always" | "never" | "auto"). get_instructions() now exports an object with dimensions, constants, instructions, and nested function bodies.
  • Compiled evaluator libraries export their input and output dimensions. Loading reads this metadata, and evaluation validates buffer and batch sizes. Replacing a compiled library at the same path is detected.
  • Precision evaluators reject invalid input counts and unsupported precision with Python exceptions. Improved preservation of constant precision, evaluation at integer Bessel orders, and transcendental calls in generated C++.
  • Updated the SymJIT dependency to 2.25. Direct Rust expression evaluation now also accepts standard-library HashMap values.

Numerica 3.0

  • Integer and floating-point backends can be selected independently: integer-gmp or integer-malachite, and float-mpfr or float-astro. GMP integers and MPFR floats remain the defaults.
  • Redesigned compact integer storage, improved integer factorization, and added faster integer, rational, and finite-field polynomial kernels, including checked division kernels.
  • Added certified real and complex interval arithmetic for enclosures and root certification.
  • Separated mathematical ordering (OrderedRing, RealEmbedding) and random sampling (SampleableRing) from general ring operations. Added explicit integer-range sampling and compatible RNG re-exports.
  • Improved complex arithmetic accuracy, decimal parsing, precision validation, and NaN construction that preserves numeric shape. Corrected zero powers in finite fields.
  • Corrected matrix inversion and sparse identity checks, validated dense and sparse matrix structure, and added approximate integer lattice-basis reduction.
  • Integration grids validate dimensions, bin counts, and sampling settings. Rust continuous and discrete grid constructors now return Result.

Builds and licensing

  • Symbolica uses Numerica 3.0 and Graphica 3.0. The minimum Rust version remains 1.89.
  • Added WebAssembly builds with the wasm feature. Native compilation and JIT support are controlled by native_code_generation, enabled by default.
  • Introduced the Symbolica Source-Available License 1.0. Numerica remains MIT licensed. See the license information and the license supplied with each package.
  • Updated signed offline license keys and moved Rust licensing APIs into symbolica::license. Added signed library keys for scoped multicore unlocking in Python packages and Rust libraries.