Skip to content

Symbolica 3.0: symbolic computing in your browser

Run Symbolica in your browser, compute inside Typst documents, and solve nonlinear systems exactly. Try the interactive examples in this release post!

Symbolica is a fast computer algebra system built in Rust with an extensive Python API. Version 3.0 brings it to the browser: you can edit and run the examples in this post without installing anything!

The release also adds particle physics tools, exact nonlinear equation solving, algebraic numbers, precision-aware Python scalars, and a Typst plugin for computing directly inside documents. Below are the highlights, with examples you can try.

For changes to existing code, see the migration guide. The detailed changelog is at the end.

Run Symbolica in your browser

Symbolica now compiles to WebAssembly (WASM). Combined with Pyodide, this lets Python notebooks run entirely in your browser. No Python installation or remote computation server is needed.

Click the Run button for a demonstration (downloads 15 MB):

from symbolica import *
x = S('x')

E('exp(x^2) * sin(x)').integrate(x)

You can edit the code as you please and run it again. Hovering over integrate reveals its signature and documentation (through a WASM version of ty). The output is MathML, a markup language for math that all modern browsers can render.

Rendering a 1GB expression would freeze the browser. Therefore, large expressions are automatically wrapped in a custom pager that shows a bounded preview. The pager lets you inspect or expand individual parts. Try it on this product:

from symbolica import E, S
x, y, z, w, a = S('x', 'y', 'z', 'w', 'a')

((x+y+z+w)**20).expand() * (1+a)

Click a numbered marker, then Show more to reveal more of the expression. The full expression stays in the browser's Rust/WASM runtime and only parts are streamed to the browser. The output adapts to the available width on your device, and reacts to resizing.

Most examples in the Symbolica documentation are now editable and runnable. On the newly created Gallery page you will find full Marimo notebooks with interactive controls, and some examples for tensor manipulation and particle physics.

I expect these notebooks to be especially useful for teaching and sharing research results that readers can explore themselves.

Typst plugin

WebAssembly also makes Symbolica available inside Typst, a modern typesetter. The plugin is free to use, including commercially, and exposes most of Symbolica's API for symbolic algebra, equation solving, and numerical evaluation.

For example, factor a polynomial and differentiate it while typesetting:

#import "@preview/symbolica:0.1.0": *

#let x = symbol("x")
#let f = math($x^4 - 5 x^2 + 4$)

$
  f(x) &= #to-typst(f) \
       &= #to-typst(factor(f)) \
  f'(x) &= #to-typst(derivative(f, x))
$

This produces

f(x)=x4−5x2+4=(x−2)(x−1)(x+1)(x+2)f′(x)=4x3−10x \begin{aligned} f(x) &= x^4 - 5x^2 + 4 \\ &= (x - 2)(x - 1)(x + 1)(x + 2) \\ f'(x) &= 4x^3 - 10x \end{aligned}

The computation and its typeset result stay together, avoiding a separate calculation and a manual copy into the document. See the manual for the full API.

HEPkit: particle-physics tools that work together

Particle-physics calculations often involve several specialised programs, each with its own installation, input language, conventions and quirks. HEPkit brings these tools into a shared framework built around Symbolica. Together with Lucien Huber, Valentin Hirschi, and other community members, we are rebuilding established tools in Rust with a typed Python interface.

HEPkit connects model definitions, Feynman diagrams, and tensor expressions. Its Rust components share a Symbolica kernel, allowing the stages of a calculation to exchange mathematical objects directly.

The toolkit covers graph generation, tensor and integration-by-parts reductions, UV expansions, auxiliary mass flow, and more. These components can work together in one self-contained notebook.

Choose a two-loop QED diagram and inspect its numerator in the notebook below.

Diagrams have interactive SVG views, and tensor expressions use compact mathematical notation.

The HEPkit website has more examples and a fuller list of capabilities. This is an ongoing effort: we are porting existing tools to Rust with AI assistance, checking results against their original implementations, improving performance and addressing shortcomings.

HEPkit also collects citations for the methods used in your calculation.

Exact algebra and solving

Symbolica 3.0 can represent polynomial roots exactly and use them to solve nonlinear systems, including systems with symbolic parameters. You can keep a result exact during algebraic manipulations and evaluate it numerically when needed.

The new root object selects a root of a polynomial. Real roots come first, in ascending order, followed by complex roots ordered by real and then imaginary part.

Automatic simplification

Constructing a root simplifies its defining polynomial and returns an integer or radical when possible. For example:

from symbolica import S

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

The result is

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

A root that has no simpler form remains exact. You can still evaluate it at the precision you need:

from symbolica import S

x = S("x")
alpha = (x**3 - x - 1).root(0)
display(alpha)
alpha.to_float(80)

Nonlinear systems with parameters

Exact roots also make it possible to express solutions that cannot be written with radicals. The solver keeps track of parameter conditions and supports algebraic-number coefficients.

For example, we can solve the coupled system

x2+y2=a,xy=b x^2 + y^2 = a, \qquad xy = b

for xx and yy, keeping aa and bb 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])
solutions

For generic parameter values, the four solution branches can be written as:

tk=root⁡(t4−at2+b2, k),k=0,1,2,3,(x,y)=(atk−tk3b,  tk). \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 returned solutions record the conditions on aa and bb under which each branch is valid.

The default domain is complex; you can also ask for real or rational solutions.

Algebraic-number coefficients

Algebraic numbers can now be coefficients of polynomials, rather than being treated as extra symbolic variables. For example, this conversion discovers the extension containing 2\sqrt{2}:

from symbolica import N, S

x = S("x")
expr = x + N(2).sqrt()
expr.to_polynomial(extensions=[])**2

You can also choose an algebraic extension explicitly when factoring:

from symbolica import N, S

x = S("x")
expr = x**2 - 6
expr.factor(extension=[N(2).sqrt(), N(3).sqrt()])

The result is (x−6)(x+6)(x - \sqrt{6})(x + \sqrt{6}).

Numerical evaluation

High-precision results now come back to Python as Float and ComplexFloat objects. They carry precision through arithmetic, elementary functions, and formatting, so you can continue calculating with a numerical result without converting it to another library's type. Python's Decimal class inputs remain supported.

We have also added a benchmark page that compares Symbolica's numerical evaluation to other backends. Below we showcase an example that has nested polynomials, exponentials, sines, and cosines. Symbolica generates code faster, uses less build memory, and evaluates faster than the other tested backends.

Nested analytic expression benchmark comparing evaluation time, code-generation time, and peak build memory across Symbolica, Mathematica, and FORM. Nested analytic expression benchmark comparing evaluation time, code-generation time, and peak build memory across Symbolica, Mathematica, and FORM.

Creating the browser build

Most of Symbolica's code is pure Rust and most algebra is done in-house. The exception is arbitrary-precision arithmetic. Native Symbolica uses GMP and MPFR, two reliable and fast C libraries.

By introducing an abstraction layer over the integer and floating-point backends, we could swap out GMP and MPFR with Malachite for integers and Astro-float for floating-point numbers. These libraries are currently a bit slower than GMP/MPFR, but they are written in pure Rust and can more easily be compiled to WASM.

The browser build has some limitations: there is little support for 64-bit WASM compilation in Rust, so the whole build is 32-bit (a 4 GB addressing space). Large expressions may exceed this limit, but Symbolica is optimized for a small memory footprint, so you will likely not run into this problem very soon (see also the benchmarks). On top, Pyodide does not support threading.

Fortunately, it is easy to deploy a notebook locally. Just run

uvx --refresh --python 3.14 --with symbolica marimo edit yournotebook.py

WASM is surprisingly fast for parsing and algebra: these workloads took about 1.4–2 times as long as native after warmup:

Case Native WASM WASM / native
Parse a 496-term expression 0.511 ms 0.699 ms 1.37×
Expand (1+x+y+z)^30 (5,456 terms) 6.2 ms 9.41 ms 1.52×
Factor x^12-y^12 0.284 ms 0.44 ms 1.55×
Differentiate a 5,456-term expression 6.2 ms 8.79 ms 1.42×
Multiply dense polynomials (455 × 364 terms) 0.8 ms 1.6 ms 2.00×
Exact polynomial division (1,540 ÷ 120 terms) 0.776 ms 1.26 ms 1.63×
Evaluate 496 terms (one point) 1.01 µs 3.93 µs 3.87×
Evaluate 496 terms (batch of 256; per point) 0.0751 µs 2.54 µs 33.77×

Evaluation uses prebuilt evaluators and preallocated NumPy inputs; Python-call and output-allocation costs are included. The batch row reports time per point. Evaluator construction and the first call took a combined median of 6.63 ms natively and 55 ms in WASM, excluded from the warmed timings. See the methodology and raw measurements for the matching Symbolica 3.0.2 builds, setup breakdown, and all samples.

Numerical evaluation shows a larger gap, especially for batches. Native evaluators use JIT compilation; the WASM build currently interprets the optimized evaluation instructions. In the next Symbolica version, WASM will also be supported as a JIT target (using symjit-wasm).

Closing

Symbolica is now accessible through multiple platforms: you can run it directly in your browser and inside Typst documents. We are very excited about the new use cases that this unlocks.

Try the live examples in the gallery!

Appendix Detailed changelog All changes since Symbolica 2.2

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.
  • Root indices put real roots first, in ascending order, followed by complex roots ordered by real and then imaginary part. Root isolation and numerical root finding handle difficult and degenerate cases more reliably.
  • 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.
  • Fixed resultant signs and vanishing subresultants, zero generators in F4 reduction, algebraic constant coefficients, finite-field factor descent, and inconsistent sparse linear systems.
  • Added Rust polynomial and rational reconstruction algorithms, with shared resource budgets and propagation of errors from black-box evaluations.

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 with negative indices and Python slices of immediate children. Arithmetic can defer to a custom right operand's reflected operators, supporting Community types without discarding them.
  • Added configurable check_zero() identity tests with finite-field and numerical sampling, precision limits, exact fallback, and an explicit choice to treat stored floating coefficients as exact. Results can be undecidable.
  • Pruned impossible pattern matches earlier while preserving required wildcard minima in partial matches, and improved selective expansion and normalization.
  • Series coefficient access distinguishes known zero coefficients from terms outside the computed order. Fixed fractional-order conversions and truncation of empty series; expansions can also use wildcard variables.
  • 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.
  • Symbol printers accept per-mode dictionaries as well as callbacks. Symbols with attached data can be parsed, and cyclic printing is canonicalized.
  • 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.
  • Stateful serialization rebuilds imported symbol identities and preserves expression-valued tags. Evaluator saves include the symbolic state needed for loading in another thread or process; external callbacks remain separate.

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.
  • Added Evaluator.from_instructions() to reconstruct an evaluator, including retained function bodies. Function names and tags remain Expression objects. EvaluatorInstructions.constant_functions exposes registered constants and fixed-argument calls as EvaluatorConstantFunction objects, so code generators can evaluate them at their target precision.
  • NumPy is optional for Python evaluation. Machine-precision batch results are NumPy arrays when it is installed and nested Python lists otherwise.
  • 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++.
  • Fixed constant-function handling, complex and SIMD code generation, register reuse, deserialization, and deeply nested evaluator failures. Retained function bodies survive differentiation and receive shared optimizations.
  • Added Rust tracked numerical evaluation and harmonic/generalized polylogarithm periods, including sixth-root and endpoint-one evaluations.
  • Updated the SymJIT dependency to 2.25. Direct Rust expression evaluation now also accepts standard-library HashMap values.

Notebook output and Community APIs

  • Added shared bounded MathML output for expressions, polynomials, series, rational functions, and matrices. MathMLViewer, MathMLPageSource, MathMLPage, and MathMLMarker support custom providers and a standalone browser renderer without requiring widget packages for page generation.
  • formatted() uses paged MathML by default; mathml=False selects the highlighted text view. Custom printers can return bounded MathML fragments for PrintMode.MathML.
  • Added responsive wrapping, inspection, expansion/collapse, and keyboard zoom. Omission markers open at the hidden items, compact products can fit completely in automatic previews, and explicit page sizes remain bounded.
  • Corrected signs, fraction sizing, continuation operators, matrix paging, radical grouping, and omission placement. Compact tensor indices and trailing factors remain visible when they fit; cached pages and fewer layout passes reduce repeated rendering work.
  • Community tensor expressions preserve ordered external axes through their tensor-aware operations. Use outer or explicit port contractions to control how axes combine, to_expression() to drop the tensor interface, and paged() to retain tensor notation in interactive output.
  • HEPkit graph output supports native SVG views and isolated interactive notebook display. Tensor, graph, and integration components share the same Symbolica kernel and typed extension registration.

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.
  • Fixed determinant row-swap signs, signed integer shifts and serialization, finite-field sampling, and complex powers and division. Improved transcendental precision and error estimates, preserving uncertainty across working-precision changes and mixed-precision arithmetic.

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.
  • Legacy license keys are processed with a migration warning. Dependent Rust libraries can capture and activate InheritedLibraryUnlock handles in their workers, avoiding Python GIL deadlocks during license checks.
  • Added typed extension points for optional Python implementations linked into one native host, and the separate c_api feature gate for the C API.

Discussion