Numerical evaluation
Symbolica supports several levels of numerical evaluation. Use to_float when you want a symbolic expression with numerical constants, evaluate for one-off numerical substitution, and an evaluator when the same expression will be evaluated many times.
Convert constants with to_float
to_float converts rational coefficients and built-in constants/functions to floating-point numbers while keeping unresolved symbols symbolic. This is useful when you want to inspect or continue manipulating an approximate expression.
from symbolica import *
expr = E('x + 1/3 + pi + exp(1)')
print(expr.to_float(20))use symbolica::prelude::*;
fn main() {
let expr = parse!("x + 1/3 + pi + exp(1)");
println!("{}", expr.to_float(20));
}Output
6.1932078153821718071+x
to_float is not a replacement for an evaluator. It performs symbolic conversion once; evaluators build an optimized numerical program.
Single evaluation
For a one-off numerical substitution, use evaluate. In Python, the constants map may contain variables and fully applied user functions. In Rust, the map keys are atoms and the values determine the numeric domain.
from symbolica import *
x, f = S('x', 'f')
expr = E('cos(x)*3 + f(2)')
print(expr.evaluate({x: 1, f(2): 4.0}))
print(expr.evaluate({x: 1 + 2j, f(2): 4.0}))use ahash::HashMap;
use symbolica::prelude::*;
fn main() {
let expr = parse!("cos(x)*3 + f(2)");
let mut constants = HashMap::default();
constants.insert(parse!("x"), Complex::new(1.0, 0.0));
constants.insert(parse!("f(2)"), Complex::new(4.0, 0.0));
println!("{}", expr.evaluate(&constants).unwrap());
let mut constants = HashMap::default();
constants.insert(parse!("x"), Complex::new(1.0, 2.0));
constants.insert(parse!("f(2)"), Complex::new(4.0, 0.0));
println!("{}", expr.evaluate(&constants).unwrap());
}Output
(5.620906917604419+0i)
(10.098169021058997+-9.155693397455401i)
For arbitrary precision in Python, pass decimal_digit_precision to evaluate. In Rust, use evaluate_with_prec.
Evaluators
Naive evaluation repeatedly walks the expression tree. For Monte Carlo integration, fitting, scans, and generated kernels, build an evaluator instead. Evaluators rewrite expressions into an instruction program, optimize it with Horner schemes and common subexpression elimination, recycle temporaries, and can be JIT-compiled through SymJIT.
In Python, evaluators JIT compile on first use by default. In Rust, build the evaluator, map the exact rational coefficients to a numerical domain, and optionally JIT compile it.
from symbolica import *
import numpy as np
x, y = S('x', 'y')
ev = E('x*y + sin(x)^2').evaluator([x, y])
points = np.array([1.0, 2.0, 3.0, 4.0]).reshape((2, 2))
print(ev.evaluate(points))use symbolica::prelude::*;
fn main() {
let params = [parse!("x"), parse!("y")];
let mut ev = parse!("x*y + sin(x)^2")
.evaluator(¶ms)
.build()
.unwrap()
.map_coeff(&|c| c.re.to_f64());
let mut out = [0.0];
ev.evaluate(&[1.0, 2.0], &mut out);
println!("{:?}", out);
}Output
[[ 2.70807342]
[16.01991486]]
Nested expressions
Evaluators can inline nested symbolic functions. Provide function definitions with functions in Python, or with add_function on the Rust builder.
For example, evaluate:
\[ \begin{align*} e &= x + \pi + \cos(x) + f(g(x+1),2x),\\ f(y,z) &= y^2 + z^2 y^2,\\ g(y) &= y + 5. \end{align*} \]
from symbolica import *
x, y, z, pi, f, g = S('x', 'y', 'z', 'pi', 'f', 'g')
expr = E('x + pi + cos(x) + f(g(x+1), x*2)').replace(pi, N(22)/7)
f_body = E('y^2 + z^2*y^2')
g_body = E('y + 5')
ev = expr.evaluator([x], functions={(f, (y, z)): f_body, (g, (y,)): g_body})
print(ev.evaluate([[5.0]]))use symbolica::prelude::*;
fn main() {
let (y, z, pi, f, g) = symbol!("y", "z", "pi", "f", "g");
let expr = parse!("x + pi + cos(x) + f(g(x+1), x*2)");
let mut ev = expr
.replace(pi)
.with(parse!("22/7"))
.evaluator(&[parse!("x")])
.add_function(f, vec![y, z], parse!("y^2 + z^2*y^2"))
.unwrap()
.add_function(g, vec![y], parse!("y + 5"))
.unwrap()
.build()
.unwrap()
.map_coeff(&|c| c.re.to_f64());
println!("{}", ev.evaluate_single(&[5.0]));
}Output
12229.42651932832
If the function name in a Python functions key is itself a function, its arguments are treated as symbolic tags. In Rust, use add_tagged_function for the same pattern.
Multiple expressions
Build one evaluator for multiple outputs when expressions share subexpressions. Symbolica optimizes the outputs together and computes shared work once.
from symbolica import *
x = S('x')
e1 = x*x + 5
e2 = x*x + 6
ev = Expression.evaluator_multiple([e1, e2], [x])
print(ev.evaluate([[5.0]]))use symbolica::prelude::*;
fn main() {
let exprs = [parse!("x^2 + 5"), parse!("x^2 + 6")];
let mut ev = Atom::evaluator_multiple(&exprs, &[parse!("x")])
.build()
.unwrap()
.map_coeff(&|c| c.re.to_f64());
let mut out = [0.0, 0.0];
ev.evaluate(&[5.0], &mut out);
println!("{out:?}");
}Output
Python:
[30.0, 31.0]
Branching
The built-in if(condition, true_branch, false_branch) can be used in evaluators. The condition is false only when it evaluates to zero.
from symbolica import *
x, y, z = S('x', 'y', 'z')
ev = E('if(y, x*x + z*z + x*z*z, x*x + 3)').evaluator([x, y, z])
print(ev.evaluate([[3.0, 1.0, 2.0]]))
print(ev.evaluate([[3.0, 0.0, 2.0]]))use symbolica::prelude::*;
fn main() {
let params = [parse!("x"), parse!("y"), parse!("z")];
let mut ev = parse!("if(y, x*x + z*z + x*z*z, x*x + 3)")
.evaluator(¶ms)
.build()
.unwrap()
.map_coeff(&|c| c.re.to_f64());
println!("{}", ev.evaluate_single(&[3.0, 1.0, 2.0]));
println!("{}", ev.evaluate_single(&[3.0, 0.0, 2.0]));
}Output
25
12
Evaluation hooks
Symbols can register numerical implementations. This is the 2.0-style way to teach evaluators about externally implemented functions.
from symbolica import *
double = S(
'double',
eval={
'float': lambda args: 2.0 * args[0],
'complex': lambda args: 2.0 * args[0],
},
)
x = S('x')
ev = double(x).evaluator([x])
print(ev.evaluate([[3.0]]))use symbolica::prelude::*;
fn main() {
let _double = symbol!(
"double",
eval = EvaluationInfo::new().register(|args: &[f64]| 2.0 * args[0])
);
let mut ev = parse!("double(x)")
.evaluator(&[parse!("x")])
.build()
.unwrap()
.map_coeff(&|c| c.re.to_f64());
println!("{}", ev.evaluate_single(&[3.0]));
}Output
6
Rust EvaluationInfo can register several numeric domains on the same symbol, including f64, Complex<f64>, arbitrary-precision Float, and tagged functions. See Symbols for the full hook list.
JIT compilation
In Python, JIT compilation is enabled by default and happens lazily on first evaluate or evaluate_complex. You can control it on the evaluator.
from symbolica import *
x, y = S('x', 'y')
ev = E('x*y + sin(x)^2').evaluator([x, y])
ev.jit_compile(True, direct_translation=True, optimization_level=3)
print(ev.evaluate([[1.0, 2.0]]))use symbolica::prelude::*;
fn main() {
let params = [parse!("x"), parse!("y")];
let mut ev = parse!("x*y + sin(x)^2")
.evaluator(¶ms)
.build()
.unwrap()
.map_coeff(&|c| c.re.to_f64())
.jit_compile(JITCompilationSettings::new().direct_translation(true))
.unwrap();
let mut out = [0.0];
ev.evaluate(&[1.0, 2.0], &mut out);
println!("{}", out[0]);
}Output
2.708073418273571
direct_translation skips tree-based JIT analysis and translates Symbolica instructions directly to SymJIT IR. This is often a good default for already optimized evaluator programs.
Automatic differentiation
Evaluators support fast automatic differentiation. Provide the derivative shape once, and Symbolica vectorizes the evaluator so all requested components are optimized together.
For first derivatives in x and y, use three components: the value, d/dx, and d/dy.
from symbolica import *
x, y = S('x', 'y')
ev = E('x^2 + y*x').evaluator([x, y])
ev.dualize([[0, 0], [1, 0], [0, 1]])
print(ev.evaluate([[2.0, 1.0, 0.0, 3.0, 0.0, 1.0]]))Output
[[10. 7. 2.]]
The flattened input contains all dual components for all parameters. The example evaluates at x = 2, y = 3, with unit derivative components for x and y.
Code generation
Evaluators can be exported to C++ and compiled to shared libraries. Python exposes this through Evaluator.compile; Rust exposes export_cpp, compile, and load.
By default the code generation generates optimized inline assembly. This prevents long compilation times for large expressions.
from symbolica import *
x = S('x')
ev = E('x^2 + 1').evaluator([x])
compiled = ev.compile('my_fun', 'test.cpp', 'test.so', 'real')
print(compiled.evaluate([[5.0]]))use symbolica::prelude::*;
fn main() {
let params = [parse!("x")];
let ev = parse!("x^2 + 1")
.evaluator(¶ms)
.build()
.unwrap()
.map_coeff(&|c| c.re.to_f64());
let code = ev
.export_cpp::<f64>("test.cpp", "my_fun", ExportSettings::default())
.unwrap();
let lib = code.compile("test.so", f64::get_default_compile_options()).unwrap();
let mut compiled = lib.load().unwrap();
let mut out = [0.0];
compiled.evaluate(&[5.0], &mut out);
println!("{}", out[0]);
}The number_type in Python can be real, complex, real_4x, complex_4x, cuda_real, or cuda_complex. Rust uses the type parameter of export_cpp, for example f64, Complex<f64>, SIMD types, or CUDA evaluator types.