Migrating from Symbolica 1.5 to 2.0

Update Symbolica 1.5 code to Symbolica 2.0, including Python symbol hooks, evaluators, printing, series, and Rust API changes.

This guide covers the main user-facing changes between Symbolica 1.5.x and Symbolica 2.0.0. It focuses on code that is likely to require edits: Python symbol construction, expression evaluation, optimized evaluators, printing, series, and the Rust evaluator API.

Upgrade Checklist

  1. Upgrade the package:

    pip install --upgrade symbolica

    For Rust:

    cargo add symbolica@2
  2. If you use Rust, update to Rust 1.89 or newer.

  3. Replace Python custom symbol keyword arguments:

    1.5 keyword 2.0 keyword
    custom_normalization normalization
    custom_print print
    custom_derivative derivative
  4. Update direct Python evaluation calls:

    • Expression.evaluate(constants, functions) is now Expression.evaluate(constants).
    • Substitute function calls directly in the constants map, for example {f(2): 4.0}.
    • Expression.evaluate_complex(...) was folded into Expression.evaluate(...).
    • Use Expression.evaluate(constants, decimal_digit_precision) for arbitrary precision expression evaluation.
  5. Update optimized Python evaluator construction:

    • expr.evaluator(constants, functions, params, ...) is now expr.evaluator(params, functions=..., ...).
    • Constant substitutions are no longer a separate evaluator-construction argument. Treat fixed values as expressions before creating the evaluator, or include them as parameters if they vary.
    • Python external functions should be registered on symbols through S(..., eval={...}), not through external_functions=....
  6. Update Rust evaluator construction:

    • expr.evaluator(&fn_map, &params, settings) is now expr.evaluator(&params).function_map(fn_map).optimization_settings(settings).build().
    • Atom::evaluator_multiple(...) likewise returns an EvaluatorBuilder.
    • jit_compile() now takes JITCompilationSettings.
  7. Replace old print options:

    • square_brackets_for_function=True was removed.
    • Use function_brackets=("[", "]").
    • custom_print_mode now accepts structured data: dict[str, int | str | dict[str | int, Any]], not just an integer.
  8. If you consumed evaluator instruction tuples from get_instructions(), update your parser. Function-call instructions now include tag arguments and realness metadata.

Python API Changes

Symbol construction

The custom hook keyword arguments were shortened.

from symbolica import *

x_, x1_ = S("x_", "x1_")

# 1.5
real_log = S(
    "real_log",
    custom_normalization=T().replace(E("x_(exp(x1_))"), x1_),
    custom_print=lambda *args, **kwargs: "real_log",
    custom_derivative=lambda expr, index: expr,
)

# 2.0
real_log = S(
    "real_log",
    normalization=T().replace(E("x_(exp(x1_))"), x1_),
    print=lambda *args, **kwargs: "real_log",
    derivative=lambda expr, index: expr,
)

Symbolica 2.0 also adds series= and eval= hooks for custom series expansion and numeric evaluation.

import cmath
import math
from decimal import Decimal
from symbolica import *

def inv_series(args):
    return (N(0), args[0].pow(-1).to_expression())

inv = S("inv", series=inv_series)

my_cosh = S(
    "my_cosh",
    eval={
        "float": lambda args: math.cosh(args[0]),
        "complex": lambda args: cmath.cosh(args[0]),
        "decimal": lambda args: args[0].exp() / Decimal(2)
        + (-args[0]).exp() / Decimal(2),
    },
)

Direct expression evaluation

In 1.5, direct evaluation accepted a constants map and a function map. In 2.0, direct expression evaluation accepts one map. Function calls with known arguments are substituted as constants.

from symbolica import *

x, f = S("x", "f")
expr = E("3*cos(x)") + f(2)

# 1.5
value = expr.evaluate({x: 1.0}, {f: lambda args: args[0] + 2.0})

# 2.0
value = expr.evaluate({x: 1.0, f(2): 4.0})

Complex and arbitrary-precision direct evaluation now use the same entry point.

from decimal import Decimal
from symbolica import *

x = S("x")
expr = E("sqrt(x)")

z = expr.evaluate({x: 1 + 2j})
hi_prec = expr.evaluate({x: Decimal("2.0")}, 80)

Optimized evaluators

Evaluator construction now starts with the parameter list. Function definitions, when needed, are passed by keyword.

from symbolica import *

x, y, z, f, g = S("x", "y", "z", "f", "g")
expr = E("x + f(g(x + 1), 2*x)")

fd = E("y^2 + z^2*y^2")
gd = E("y + 5")

# 1.5
ev = expr.evaluator({}, {(f, "f", (y, z)): fd, (g, "g", (y,)): gd}, [x])

# 2.0
ev = expr.evaluator([x], functions={(f, (y, z)): fd, (g, (y,)): gd})

The built-in conditional function no longer needs explicit registration:

from symbolica import *

x, y = S("x", "y")
ev = E("if(y, x + 1, x + 2)").evaluator([x, y])

Python-level external numeric functions moved to symbol evaluation metadata.

import math
from symbolica import *

my_square = S("my_square", eval={"float": lambda args: args[0] ** 2})
x = S("x")
ev = my_square(x).evaluator([x])

Evaluator execution

Evaluator.evaluate(...) and Evaluator.evaluate_complex(...) still evaluate batches. For best performance, pass NumPy arrays with shape (n_evaluations, n_parameters).

import numpy as np
from symbolica import *

x, y = S("x", "y")
ev = E("x*y + 2").evaluator([x, y])

out = ev.evaluate(np.array([[1.0, 2.0], [3.0, 4.0]]))

Evaluator.jit_compile(...) now also accepts optional JIT settings:

ev.jit_compile(True, direct_translation=True, optimization_level=3)

Evaluator.dualize(...) no longer accepts an external function map. Non-built-in functions are rewritten to suffixed vector functions. Register the required numeric behavior on symbols through eval=....

Polynomial conversion

to_polynomial() and to_rational_polynomial() were simplified. They now accept an optional variable order directly.

from symbolica import *

x, y = S("x", "y")
expr = E("x*y + 2*x + x^2")

poly = expr.to_polynomial([x, y])
rat = expr.to_rational_polynomial([x, y])

Conversions to number-field and finite-field polynomials are still overloads of to_polynomial(...).

Replacement

Replacement APIs were consolidated. Prefer Expression.replace(...) and Transformer.replace(...) with keyword options instead of constructing old settings objects.

from symbolica import *

x_, f = S("x_", "f")
expr = f(1) + f(2) + f(3)

expr.replace(f(x_), f(x_ + 1), once=True)
expr.replace(f(x_), f(x_ + 1), repeat=True)

allow_new_wildcards_on_rhs=True is available when the right-hand side intentionally introduces wildcards not present in the left-hand side.

Series

Series expansion uses the shorter call shape:

from symbolica import *

x = S("x")
expr = E("cos(x)/(x+1)")

# 1.5
series = expr.series(x, N(0), 3, True)

# 2.0
series = expr.series(x, 0, 3)

For rational powers, pass depth_denom.

series = expr.series(x, 0, 3, depth_denom=2)

Printing and rich display

Notebook display is richer in 2.0. Expressions, polynomials, rational polynomials, matrices, series, and graphs expose HTML and LaTeX display hooks. Use .formatted(...) when you want a rich display object explicitly.

from symbolica import *

x = S("x")
expr = E("x^2 + 2*x + 1")

plain = expr.format()
rich = expr.formatted()
latex = expr.to_latex()

Function brackets are now controlled through function_brackets:

expr.format(function_brackets=("[", "]"))

Graph API rename

Graph.generate(...) renamed the first argument from external_nodes to external_edges.

Graph.generate(
    external_edges=external_edges,
    vertex_signatures=vertex_signatures,
)

Integer utilities

Integer.is_prime(n) now accepts an optional Miller-Rabin iteration count:

Integer.is_prime(n, k=24)

Rust API Changes

Imports and prelude

Symbolica 2.0 adds a prelude with the common traits, constructors, domains, evaluator types, printing types, polynomial types, and transcendental extension trait.

use symbolica::prelude::*;

This is the recommended import style for examples and applications.

Evaluators use a builder

The Rust evaluator API now returns an EvaluatorBuilder.

use symbolica::prelude::*;

let x = parse!("x");
let expr = parse!("x*cos(x)");

// 1.5
// let ev = expr.evaluator(&FunctionMap::new(), &[x.clone()], OptimizationSettings::default())?;

// 2.0
let ev = expr
    .evaluator(&[x.clone()])
    .optimization_settings(OptimizationSettings::default())
    .build()?;

For multiple outputs:

let ev = Atom::evaluator_multiple(&[expr1, expr2], &[x])
    .optimization_settings(OptimizationSettings::default())
    .build()?;

Function maps changed shape

FunctionMap::add_function and FunctionMap::add_tagged_function no longer take the extra printable-name argument.

use symbolica::prelude::*;

let mut functions = FunctionMap::new();
functions.add_function(symbol!("f"), vec![symbol!("x")], parse!("x^2"))?;

let ev = parse!("f(x)")
    .evaluator(&[parse!("x")])
    .function_map(functions)
    .build()?;

For numeric custom functions, prefer registering evaluation behavior on the symbol:

use symbolica::prelude::*;

let _ = symbol!(
    "g",
    eval = EvaluationInfo::new()
        .register(|args: &[f64]| args[0] + 2.0)
        .register(|args: &[Complex<f64>]| args[0] + 2.0)
);

Direct evaluation

Direct Rust evaluation now takes one map of constants or known function calls.

use ahash::HashMap;
use symbolica::prelude::*;

let expr = parse!("v1*cos(v1) + f1(1)^2");

let mut constants = HashMap::default();
constants.insert(parse!("v1"), 6.0);
constants.insert(parse!("f1(1)"), 7.0);

let value = expr.evaluate(&constants)?;

Use evaluate_with_prec(&constants, precision) for arbitrary-precision evaluation.

JIT settings

Rust JIT compilation now takes JITCompilationSettings.

let mut jit = ev
    .map_coeff(&|x| x.re.to_f64())
    .jit_compile(
        JITCompilationSettings::new()
            .direct_translation(true)
            .optimization_level(2),
    )?;

Transcendental functions

The new TranscendentalFunctions trait provides method-style access to transcendentals such as trigonometric, hyperbolic, gamma, zeta, Bessel, and polylog functions. It is included in symbolica::prelude::*.

use symbolica::prelude::*;

let x = parse!("x");
let expr = x.sin() + x.gamma();

Polynomial conversion

to_polynomial and to_rational_polynomial now take the target field and an optional variable map/order directly.

use symbolica::prelude::*;

let expr = parse!("x*y + 2*x + x^2");
let poly = expr.to_polynomial::<_, u32>(&Q, [symbol!("x"), symbol!("y")]);
let rat = expr.to_rational_polynomial::<_, _, u32>(&Q, &Z, None);

Series

Rust series expansion now mirrors the simpler Python shape.

use symbolica::prelude::*;

let x = symbol!("x");
let expr = parse!("cos(x)/(x+1)");
let series = expr.series(x, 0, 3)?;

Behavioral Notes

  • Symbolica now depends on numerica 2.0 and graphica 2.0.
  • The minimum supported Rust version is now 1.89.
  • The default polynomial GCD behavior changed: Hu-Monagan GCD is enabled by default when expected to be faster. The global settings include use_hu_monagan_poly_gcd and force_hu_monagan_poly_gcd.
  • Evaluator save/load includes JIT settings in the serialized form. Loading legacy evaluator bytes is supported, but external functions are not serialized.
  • get_instructions() is intended as a low-level export API. Treat tuple shapes as versioned data and update consumers when upgrading.

New APIs Worth Adopting

  • Expression.formatted(...), Polynomial.formatted(...), and rich notebook display hooks.
  • Expression.collect_by_coefficient() and Transformer.collect_by_coefficient().
  • Expression.__int__, Expression.__float__, and Expression.__complex__ for numeric conversion when possible.
  • New mathematical constants and functions, including Expression.EULER_GAMMA, zeta, gamma, polygamma, polylog, and Bessel functions.
  • Evaluator JIT options: jit_direct_translation and jit_optimization_level.
  • real_if_args_real in Evaluator.set_real_params(...) for custom evaluators that preserve realness.

Common Before/After Summary

Area 1.5 2.0
Custom symbol hooks custom_print=... print=...
Direct evaluation expr.evaluate(constants, functions) expr.evaluate(constants)
Complex direct evaluation expr.evaluate_complex(...) expr.evaluate(...)
Optimized evaluator expr.evaluator(constants, functions, params) expr.evaluator(params, functions=...)
External numeric functions external_functions=... S(..., eval={...})
Built-in if evaluator pass conditional=[S("if")] automatic
Function brackets square_brackets_for_function=True function_brackets=("[", "]")
Python graph generation external_nodes= external_edges=
Rust evaluator direct constructor arguments EvaluatorBuilder
Rust imports many module imports use symbolica::prelude::*;