Expand description
Symbolic math library for expression trees, automatic differentiation, simplification, and code generation.
arael-sym provides a lightweight computer algebra system built around a
reference-counted expression tree (E). Expressions are constructed from
symbols and constants, combined with standard arithmetic operators (which
auto-simplify), and then differentiated, evaluated, pretty-printed, or
compiled to Rust source code.
This crate is the symbolic engine behind the
arael optimization framework, where it powers
compile-time constraint differentiation and code generation. It can also
be used independently for any symbolic math task.
§Scope and limitations
arael-sym is focused on what’s needed for nonlinear optimization:
scalar expressions, differentiation, and code generation. Compared to
a full CAS like Python’s SymPy, it does not support:
- Symbolic integration
- Equation solving (solve for x)
- Symbolic matrix algebra (symbolic determinant, inverse, eigenvalues)
- Polynomial factoring, GCD, partial fractions
- Limits, series expansion, Taylor series
- Assumptions / domain reasoning (positive, real, integer)
- Pattern matching / rewrite rules
- Pretty-printing of intermediate simplification steps
§Examples
See docs/SYM.md
for the full reference with worked examples for every feature,
and examples/sym_demo.rs
for a runnable walkthrough (cargo run --example sym_demo).
The tour below hits the high points.
§Basics
The symbols! macro expands each bare identifier to
symbol("<name>") and returns a tuple – you write the name once
instead of twice per variable. The sym! macro auto-inserts
.clone() on every reused variable so the body reads as natural
math without ownership boilerplate.
Every expression has type E, defined as
struct E(Rc<Expr>). Cloning is cheap (a reference-count bump) –
the .clone() calls sym! inserts don’t duplicate the
expression tree.
use arael_sym::*;
let result = sym! {
let (x, y) = symbols!(x, y);
let f = x * y - 1.0 + pow(x, 2.0);
format!("{}", f)
};
assert_eq!(result, "x * y + x^2 - 1");§Differentiation
use arael_sym::*;
let result = sym! {
let x = symbol("x");
let f = sin(x) * x;
// Product rule + chain rule applied automatically:
format!("{}", f.diff(x))
};
assert_eq!(result, "x * cos(x) + sin(x)");§Evaluation
use arael_sym::*;
let val = sym! {
let x = symbol("x");
let f = x * x + 1.0;
let vars = std::collections::HashMap::from([("x", 3.0)]);
f.eval(&vars).unwrap()
};
assert_eq!(val, 10.0);§Code generation
use arael_sym::*;
let (code1, code2) = sym! {
let (x, y) = symbols!(x, y);
let f = sin(x) + 1.0;
let g = atan2(y, x);
(f.to_rust("f64"), g.to_rust("f32"))
};
assert_eq!(code1, "x.sin() + 1.0_f64");
assert_eq!(code2, "y.atan2(x)");§Common Subexpression Elimination (CSE)
use arael_sym::*;
sym! {
let x = symbol("x");
let shared = sin(x) * cos(x);
let e1 = shared + 1.0;
let e2 = shared * 2.0;
let (intermediates, simplified) = cse(&[e1, e2]);
for (name, val) in &intermediates {
println!("let {} = {};", name, val);
}
for s in &simplified {
println!("{}", s);
}
};
// Output:
// let __x0 = cos(x) * sin(x);
// __x0 + 1
// 2 * __x0§Vectors and Matrices
use arael_sym::*;
let dot = sym! {
let (x, y, z) = symbols!(x, y, z);
let v = SymVec::new([x, y, z]);
let w = SymVec::new([1.0, 2.0, 3.0]);
format!("{}", v.dot(&w))
};
assert_eq!(dot, "x + 2 * y + 3 * z");§Jacobian
use arael_sym::*;
let (j00, j01, j10, j11) = sym! {
let (x, y) = symbols!(x, y);
let f = vec![x * y, sin(x) + y];
let j = jacobian(&f, &["x", "y"]);
// j is 2x2: [[df0/dx, df0/dy], [df1/dx, df1/dy]]
(format!("{}", j.get(0, 0)),
format!("{}", j.get(0, 1)),
format!("{}", j.get(1, 0)),
format!("{}", j.get(1, 1)))
};
assert_eq!(j00, "y"); // d(x*y)/dx
assert_eq!(j01, "x"); // d(x*y)/dy
assert_eq!(j10, "cos(x)"); // d(sin(x)+y)/dx
assert_eq!(j11, "1"); // d(sin(x)+y)/dy§Parsing
use arael_sym::*;
let f = parse("sin(x)^2 + cos(x)^2").unwrap();
assert_eq!(format!("{}", f), "sin(x)^2 + cos(x)^2");
let vars = std::collections::HashMap::from([("x", 1.0)]);
assert!((f.eval(&vars).unwrap() - 1.0).abs() < 1e-10);§Named constants
Named constants survive simplification (unlike numeric Const which may
be folded away). Built-in: pi, epsilon, euler. Custom
constants via named_const. The sym! macro accepts pi and
epsilon as bare identifiers.
use arael_sym::*;
sym! {
let x = symbol("x");
let f = x * x + epsilon; // bare identifier, no parens needed
assert_eq!(format!("{}", f), "x^2 + epsilon");
assert_eq!(format!("{}", sin(pi).simplify()), "0");
assert_eq!(format!("{}", cos(pi).simplify()), "-1");
assert_eq!(format!("{}", ln(euler()).simplify()), "1");
};§Identity and evaluation order
The simplifier flattens and reorders additive terms, which can cause
floating-point cancellation in generated code. For example,
1 - x^2 + epsilon^2 might be reordered to -x^2 + epsilon^2 + 1,
and at x=1 the tiny epsilon^2 gets absorbed into -1 + 1 before
it can contribute.
The identity function acts as a barrier: identity(expr) evaluates
to expr and differentiates as 1, but the simplifier cannot reorder
terms across it. Codegen wraps the body in parentheses to preserve
evaluation order in the generated Rust code.
use arael_sym::*;
sym! {
let x = symbol("x");
// Without identity: terms may reorder, epsilon^2 lost at x=1
// With identity: (1 - x^2) evaluates first, then epsilon^2 is added
let safe = identity(1.0 - x * x) + epsilon * epsilon;
let code = safe.to_rust("f64");
// Body is wrapped in parens in generated code
assert!(code.contains("(-x.powf(2.0_f64) + 1.0_f64)"));
};This pattern is used internally by safe_asin and safe_acos to
keep epsilon^2 from being lost to floating-point cancellation in the
derivative 1/sqrt(1 - x^2 + epsilon^2).
§Custom functions
Define reusable symbolic functions with automatic differentiation. The factory functions return closures that can be called like regular functions.
use arael_sym::*;
sym! {
let x = symbol("x");
let square = simple_func1("square", |t| t * t);
let f = square(x + 1.0);
assert_eq!(format!("{}", f), "square(x + 1)");
assert_eq!(format!("{}", f.diff(x)), "2 * (x + 1)");
// Codegen inlines the expanded body:
assert_eq!(f.to_rust("f64"), "(x + 1.0_f64).powf(2.0_f64)");
};§Extern functions
When a function’s runtime behavior differs from its derivative (e.g. angle normalization), use extern functions. They generate a function call in codegen while differentiating through a separate symbolic body.
use arael_sym::*;
fn my_angle_diff(args: &[f64]) -> f64 {
let d = args[0] - args[1];
d - (2.0 * std::f64::consts::PI)
* (d / (2.0 * std::f64::consts::PI) + 0.5).floor()
}
sym! {
// codegen emits my_mod::angle_diff(a, b)
// differentiation uses gradient of (a - b)
// eval uses my_angle_diff
let angle_diff = extern_func2("angle_diff", "my_mod::angle_diff",
grad2(|a, b| a - b), my_angle_diff);
let (x, y) = symbols!(x, y);
let f = angle_diff(x * x, y);
assert_eq!(format!("{}", f.diff(x)), "2 * x");
assert_eq!(f.to_rust("f64"), "my_mod::angle_diff(x.powf(2.0_f64), y)");
// eval uses the native eval_fn:
let vars = std::collections::HashMap::from([("x", 0.0), ("y", 6.283185307179586)]);
assert!(f.eval(&vars).unwrap().abs() < 1e-10); // 0 - 2pi wraps to 0
};Built-in rad_diff and rad_sum are extern functions with
rollover-safe angle normalization to [-pi, pi].
§Heaviside and clamp
Pragmatic functions for optimization near numerical boundaries.
heaviside has derivative 0 everywhere (not Dirac delta).
clamp has pass-through derivative (as if clamping were not there).
use arael_sym::*;
sym! {
// clamp prevents NaN from asin outside [-1, 1]
// Note: derivative still diverges at +/-1. One can prevent it
// by providing custom derivatives with simple_func1_derivs as
// is done in the built-in safe_asin().
let my_asin = simple_func1("my_asin",
|t| asin(clamp(t, -1.0, 1.0)));
let x = symbol("x");
let f = my_asin(x);
let vars = std::collections::HashMap::from([("x", 1.5)]);
// Clamped to asin(1.0) = pi/2, no NaN
let val = f.eval(&vars).unwrap();
assert!((val - std::f64::consts::FRAC_PI_2).abs() < 1e-10);
};Re-exports§
pub use geo::vect2sym;pub use geo::vect3sym;pub use geo::matrix2sym;pub use geo::matrix3sym;pub use geo::quaternsym;pub use cse::cse;
Modules§
- cse
- Common Subexpression Elimination (CSE).
- geo
- Symbolic companion types for geometric primitives (vectors, matrices, quaternions).
Macros§
- sym
- Auto-clone macro for symbolic math expressions.
- symbols
- Create several symbolic variables at once and return them as a
tuple. Each identifier becomes a fresh
Ewhose name is that identifier stringified, sparing the caller from writing the name twice per variable.
Structs§
- E
- Symbolic expression wrapper.
- Function
Bag - An extensible registry of user-defined symbolic functions, used by
parse::parse_with_functionsto make runtime-constructed functions recognisable by the string parser. - Parse
Error - Error type for expression parsing.
- SymMat
- Symbolic matrix of expressions.
- SymVec
- Symbolic vector of expressions.
Enums§
- Expr
- Expression AST node.
- Func
Kind - Describes what kind of function behavior to use for differentiation, evaluation, and code generation.
- Function
Ref - A scalar function exported by arael-sym, discovered by name. Tagged by arity so callers can validate the argument count without a second table.
Constants§
- FUNCTIONS
- The authoritative table of scalar functions arael-sym exposes by name.
Adding a new
pub fn fooabove should add an entry here as well; every string-based dispatcher (the parser, the macro’s constraint/fit dispatchers, user-facing autocompleters) reads from this one table.
Traits§
- AsVar
Name - Types that can name a symbolic variable for operations that key
into the expression tree by name –
diff,subs,collect. Implemented for&str,String,&String, andE(when it wraps aSymnode), so you can writeexpr.diff("x")orexpr.diff(&my_symbol)and reach the same variable. The blanketvar_exprdefault builds a freshSymnode from the name; implementations onEoverride it to reuse the caller’s handle and avoid an allocation.
Functions§
- abs
- Symbolic absolute value.
- acos
- Symbolic arccosine function.
- asin
- Symbolic arcsine function.
- atan
- Symbolic arctangent function.
- atan2
- Symbolic two-argument arctangent: atan2(y, x).
- c
- Short alias for
constant. Common in math notation. - clamp
- Symbolic clamp: clamp value to [lo, hi]. Derivative passes through.
Accepts
impl Into<E>on all three args so bare numeric bounds compose naturally:clamp(x, -1.0, 1.0). - constant
- Create a numeric constant.
- cos
- Symbolic cosine function.
- cosh
- Symbolic hyperbolic cosine function.
- epsilon
- Machine epsilon $\epsilon$ (
f64::EPSILON$\approx 2.22 \times 10^{-16}$). - euler
- Euler’s number $e = 2.71828\ldots$
- exp
- Symbolic exponential function (e^x).
- extern_
func - Create an n-ary extern function: codegen emits
call_path(args...), explicit derivatives for differentiation,eval_fnfor eval. - extern_
func1 - Create a unary extern function: codegen emits
call_path(arg), explicit derivatives for differentiation,eval_fnfor eval. - extern_
func2 - Create a binary extern function: codegen emits
call_path(a, b), explicit derivatives for differentiation,eval_fnfor eval. - function_
by_ name - Look up a scalar function by its conventional name. Returns
Nonefor unrecognized names – callers typically emit a user-facing error in that case. - function_
names - Iterate over the names of every scalar function arael-sym exposes. Useful for autocomplete and “what functions are available?” queries.
- grad1
- Compute the gradient of a unary function symbolically.
Returns a closure suitable for
simple_func1_derivsorextern_func1. - grad2
- Compute the gradient of a binary function symbolically.
Returns a closure suitable for
simple_func2_derivsorextern_func2. - heaviside
- Symbolic Heaviside step function: 0 if x < 0, 1 if x >= 0.
- identity
- Identity function: $\text{identity}(x) = x$, $\frac{d}{dx} = 1$.
- jacobian
- Compute the Jacobian matrix: partial derivatives of each expression with respect to each variable.
- ln
- Symbolic natural logarithm.
- log2
- Symbolic base-2 logarithm.
- log10
- Symbolic base-10 logarithm.
- named_
const - Create a named constant with explicit display, eval, codegen, and LaTeX representations.
- parse
- Parse a string into a symbolic expression, using only built-in functions.
- parse_
with_ functions - Parse a string into a symbolic expression, consulting a
FunctionBagbefore the built-in function table. - pi
- $\pi = 3.14159\ldots$
- pow
- Symbolic power function. Auto-simplifies (e.g. x^0 = 1, x^1 = x).
Accepts
impl Into<E>for both args so bare numeric literals compose naturally:pow(x, 2.0),pow(x, 3). - rad_
diff - Rollover-safe radian difference: $(a - b)$ normalized to $[-\pi, \pi]$.
- rad_sum
- Rollover-safe radian sum: $(a + b)$ normalized to $[-\pi, \pi]$.
- safe_
acos - Safe acos with clamped domain and non-diverging derivative.
- safe_
asin - Safe asin with clamped domain and non-diverging derivative.
- safe_
atan2 - Safe atan2 with non-diverging derivatives.
- safe_
sqrt - Safe square root: clamps negative inputs to zero, non-diverging derivative.
- simple_
func - Create an n-ary custom function. Returns a closure usable as
f(vec![...]). Codegen inlines the expanded body. - simple_
func1 - Create a unary custom function. Returns a closure usable as
f(expr). Codegen inlines the expanded body. - simple_
func2 - Create a binary custom function. Returns a closure usable as
f(a, b). Codegen inlines the expanded body. - simple_
func1_ derivs - Create a unary function with explicit derivatives. Body used for eval and codegen (inlined).
- simple_
func2_ derivs - Create a binary function with explicit derivatives. Body used for eval and codegen (inlined).
- simple_
func_ derivs - Create an n-ary function with explicit derivatives. Body used for eval and codegen (inlined).
- sin
- Symbolic sine function.
- sinh
- Symbolic hyperbolic sine function.
- sqrt
- Symbolic square root.
- symbol
- Create a named symbolic variable.
- tan
- Symbolic tangent function.
- tanh
- Symbolic hyperbolic tangent function.