Skip to content

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::*;