Migrating from Symbolica 1.5 to 2.0
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
Upgrade the package:
pip install --upgrade symbolicaFor Rust:
cargo add symbolica@2If you use Rust, update to Rust 1.89 or newer.
Replace Python custom symbol keyword arguments:
1.5 keyword 2.0 keyword custom_normalizationnormalizationcustom_printprintcustom_derivativederivativeUpdate direct Python evaluation calls:
Expression.evaluate(constants, functions)is nowExpression.evaluate(constants).- Substitute function calls directly in the constants map, for example
{f(2): 4.0}. Expression.evaluate_complex(...)was folded intoExpression.evaluate(...).- Use
Expression.evaluate(constants, decimal_digit_precision)for arbitrary precision expression evaluation.
Update optimized Python evaluator construction:
expr.evaluator(constants, functions, params, ...)is nowexpr.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 throughexternal_functions=....
Update Rust evaluator construction:
expr.evaluator(&fn_map, ¶ms, settings)is nowexpr.evaluator(¶ms).function_map(fn_map).optimization_settings(settings).build().Atom::evaluator_multiple(...)likewise returns anEvaluatorBuilder.jit_compile()now takesJITCompilationSettings.
Replace old print options:
square_brackets_for_function=Truewas removed.- Use
function_brackets=("[", "]"). custom_print_modenow accepts structured data:dict[str, int | str | dict[str | int, Any]], not just an integer.
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
numerica2.0 andgraphica2.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_gcdandforce_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()andTransformer.collect_by_coefficient().Expression.__int__,Expression.__float__, andExpression.__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_translationandjit_optimization_level. real_if_args_realinEvaluator.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::*; |