Skip to main content

Crate arael_sym

Crate arael_sym 

Source
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 E whose name is that identifier stringified, sparing the caller from writing the name twice per variable.

Structs§

E
Symbolic expression wrapper.
FunctionBag
An extensible registry of user-defined symbolic functions, used by parse::parse_with_functions to make runtime-constructed functions recognisable by the string parser.
ParseError
Error type for expression parsing.
SymMat
Symbolic matrix of expressions.
SymVec
Symbolic vector of expressions.

Enums§

Expr
Expression AST node.
FuncKind
Describes what kind of function behavior to use for differentiation, evaluation, and code generation.
FunctionRef
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 foo above 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§

AsVarName
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, and E (when it wraps a Sym node), so you can write expr.diff("x") or expr.diff(&my_symbol) and reach the same variable. The blanket var_expr default builds a fresh Sym node from the name; implementations on E override 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_fn for eval.
extern_func1
Create a unary extern function: codegen emits call_path(arg), explicit derivatives for differentiation, eval_fn for eval.
extern_func2
Create a binary extern function: codegen emits call_path(a, b), explicit derivatives for differentiation, eval_fn for eval.
function_by_name
Look up a scalar function by its conventional name. Returns None for 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_derivs or extern_func1.
grad2
Compute the gradient of a binary function symbolically. Returns a closure suitable for simple_func2_derivs or extern_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 FunctionBag before 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.