Symbols

Define Symbolica symbols, namespaces, attributes, tags, aliases, and custom behavior in Python and Rust.

A symbol is the identity of a variable, a function name, or a wildcard. The same symbol can be used as a variable and as a function name, and its properties belong to the symbol itself rather than to one occurrence of it.

Symbols are registered globally. If you create a symbol without properties and it already exists, Symbolica reuses the existing definition. If you create it with explicit properties, those properties must agree with the existing definition.

Important

Define symbols with special properties before parsing or constructing expressions that use them. Once a symbol is defined, its attributes cannot be changed without invalidating existing expressions.

Create symbols

Use S or Expression.symbol in Python. In Rust, use symbol! for symbols and function! or Symbol::call to build functions.

from symbolica import *

x = S('x')
y, f = S('y', 'f')

expr = f(x + 1, y)
print(expr)
use symbolica::prelude::*;

fn main() {
    let x = symbol!("x");
    let (y, f) = symbol!("y", "f");

    let expr = function!(f, x + 1, y);
    println!("{expr}");
}

Output

f(1+x,y)

In Rust, symbol!("x") panics if an explicit definition conflicts with an existing symbol. Use try_symbol! for a fallible version, or get_symbol! if you only want to fetch a symbol that already exists.

Namespaces

Every symbol has a namespace. The full name is written as namespace::name. Namespaces let libraries define helper symbols without accidentally colliding with user symbols or with symbols from another library.

In Rust, you can use initialize! to register symbols before user code runs. In Python, set the namespace for all symbols created in a block with set_namespace.

from symbolica import *

set_namespace('my_package')

x = S('x')
external_x = S('external::x')
expr = x + external_x

print(expr)
print(expr.format(show_namespaces=True))
use symbolica::prelude::*;

fn main() {
    let expr = symbol!("my_package::x") + symbol!("external::x");

    println!("{expr}");
    println!("{expr:#}");
}

Output

x+x
my_package::x+external::x

Pretty printing hides namespaces by default. Use namespace-aware formatting when writing import-export friendly output or debugging symbol identity.

Attributes

Attributes are built-in properties that Symbolica understands. The symmetry attributes are properties of a symbol when it is used as a function. The scalar, real, integer, and positive attributes are assumptions about expressions containing the symbol.

Python keyword Rust attribute Meaning
is_symmetric=True Symmetric Sort function arguments into canonical order.
is_antisymmetric=True Antisymmetric Sort arguments and multiply by -1 for every swap; repeated arguments give 0.
is_cyclesymmetric=True Cyclesymmetric Canonicalize function arguments up to cyclic rotations.
is_linear=True Linear Make the function multilinear in each argument.
is_scalar=True Scalar Treat the symbol as scalar, so it can be moved out of linear functions.
is_real=True Real Mark expressions containing the symbol as real when the surrounding expression preserves reality.
is_integer=True Integer Mark expressions containing the symbol as integer when the surrounding expression preserves integrality.
is_positive=True Positive Mark expressions containing the symbol as positive when the surrounding expression preserves positivity.

The Symmetric, Antisymmetric, and Cyclesymmetric attributes are mutually exclusive.

from symbolica import *

fs = S('fs', is_symmetric=True)
fa = S('fa', is_antisymmetric=True)
fc = S('fc', is_cyclesymmetric=True)

print(fs(3, 2, 1))
print(fa(2, 1))
print(fc(2, 1, 3))
use symbolica::prelude::*;

fn main() {
    let fs = symbol!("fs"; Symmetric);
    let fa = symbol!("fa"; Antisymmetric);
    let fc = symbol!("fc"; Cyclesymmetric);

    println!("{}", function!(fs, 3, 2, 1));
    println!("{}", function!(fa, 2, 1));
    println!("{}", function!(fc, 2, 1, 3));
}

Output

fs(1,2,3)
fa(1,2)*-1
fc(1,3,2)

Linear and scalar symbols

A linear function distributes over sums and pulls out scalar factors from each argument. Combining Symmetric and Linear gives a dot-product-like function.

from symbolica import *

x, y, z = S('x', 'y', 'z')
a = S('a', is_scalar=True)
dot = S('dot', is_symmetric=True, is_linear=True)

print(dot(3*x + y, x + 2*y + z))
print(dot(a*x + y, z))
use symbolica::prelude::*;

fn main() {
    let _a = symbol!("a"; Scalar);
    let _dot = symbol!("dot"; Symmetric, Linear);

    println!("{}", parse!("dot(3*x+y, x+2*y+z)"));
    println!("{}", parse!("dot(a*x+y, z)"));
}

Output

3*dot(x,x)+7*dot(x,y)+3*dot(x,z)+2*dot(y,y)+dot(y,z)
a*dot(x,z)+dot(y,z)

The scalar, real, integer, and positive attributes are also queried through expression predicates.

from symbolica import *

xr = S('xr', is_real=True)
ni = S('ni', is_integer=True)
ap = S('ap', is_positive=True)

print((xr + 1).is_real())
print((ni + 2).is_integer())
print((ap + 3).is_positive())
use symbolica::prelude::*;

fn main() {
    let _xr = symbol!("xr"; Real);
    let _ni = symbol!("ni"; Integer);
    let _ap = symbol!("ap"; Positive);

    println!("{}", parse!("xr + 1").is_real());
    println!("{}", parse!("ni + 2").is_integer());
    println!("{}", parse!("ap + 3").is_positive());
}

Output

Python:
True
True
True

Rust:
true
true
true

Wildcards

Symbols whose names end in underscores are wildcards in patterns:

  • x_ matches a single atom.
  • x__ matches one or more atoms.
  • x___ matches zero or more atoms.

Wildcard attributes and tags become restrictions on what the wildcard can match. For example, a real wildcard can only match expressions that Symbolica can prove are real.

from symbolica import *

f = S('f')
xr, xr__ = S('xr', 'xr__', is_real=True)

print(f(1, 2, xr**2 + 2, 3).replace(f(xr__), 1))
use symbolica::prelude::*;

fn main() {
    let x = symbol!("x");
    let x_ = symbol!("x_");
    let x__ = symbol!("x__");
    let x___ = symbol!("x___");

    println!("{}", x.get_wildcard_level());
    println!("{}", x_.get_wildcard_level());
    println!("{}", x__.get_wildcard_level());
    println!("{}", x___.get_wildcard_level());
}

Output

Python:
1

Rust:
0
1
2
3

See Pattern matching for restrictions such as req_len, req_type, req_tag, comparisons, and custom filters.

Tags

Tags are user-defined labels. Symbolica does not assign mathematical meaning to them, but you can query them and use them in pattern restrictions. Tags have namespaces; in Python, a tag without :: is stored under the python namespace.

from symbolica import *

mass = S('mass', tags=['parameter', 'real'])
mass_ = S('mass_')

print(mass.get_tags())
print(mass.replace(mass_, 1, mass_.req_tag('parameter')))
use symbolica::prelude::*;

fn main() {
    let mass = symbol!("mass", tags = [tag!("python::parameter"), tag!("python::real")]);

    println!("{:?}", mass.get_tags());
    println!("{}", if mass.has_tag(tag!("python::parameter")) { 1 } else { 0 });
}

Output

Python:
['python::parameter', 'python::real']
1

Rust:
["python::parameter", "python::real"]
1

Aliases

Aliases are alternate names for the same symbol. They are useful for import/export compatibility or ASCII spellings of a preferred name. Aliases should live in the same namespace as the primary symbol.

from symbolica import *

field = S('physics::field_strength', aliases=['physics::F'])
expr = E('physics::F(rho,sigma) + physics::field_strength(rho,sigma)')

print(expr)
print(expr.format(show_namespaces=True))
use symbolica::prelude::*;

fn main() {
    let _field = symbol!("physics::field_strength", aliases = ["physics::F"]);
    let expr = parse!(
        "physics::F(python::rho,python::sigma) + physics::field_strength(python::rho,python::sigma)"
    );

    println!("{expr}");
    println!("{expr:#}");
}

Output

2*field_strength(rho,sigma)
2·physics::field_strength(python::rho,python::sigma)

Custom behavior

A symbol can also carry user-defined behavior. These settings are more advanced and are usually defined once by a library. In Python, custom behavior hooks are available when defining a single symbol. In Rust, they are passed as flags to symbol!.

Python keyword Rust symbol! flag Purpose
normalization norm Run a transformer when the symbol is normalized.
print print Override printing for this symbol or function. Return None to use the default printer.
derivative der Define a custom derivative rule for the function.
series series Define custom series behavior near poles.
eval eval Register numerical evaluation implementations.
data data Attach custom user data to the symbol.

Normalization

A normalization hook rewrites a symbol or function immediately after Symbolica has normalized its arguments. In Python, the hook is a Transformer. In Rust, it is a closure that can write a replacement into out.

Warning

When defining a normalization hook in Python, do not use the symbol being defined inside the transformer pattern. Use a wildcard with the same relevant attributes instead, otherwise the pattern itself may define the symbol too early.

from symbolica import *

real_log = S(
    'real_log',
    normalization=T().replace(E('x_(exp(x1_))'), E('x1_')),
)

print(E('real_log(exp(x)) + real_log(5)'))
use symbolica::prelude::*;

fn main() {
    let _even_args = symbol!("even_args", norm = |f, out| {
        if let AtomView::Fun(fun) = f {
            if fun.get_nargs() % 2 == 1 {
                out.to_num(0);
            }
        }
    });

    println!("{}", parse!("even_args(1,2)"));
    println!("{}", parse!("even_args(1,2,3)"));
}

Output

Python:
x+real_log(5)

Rust:
even_args(1,2)
0

Printing

A print hook controls how a symbol is rendered. Return None to fall back to the default printer.

from symbolica import *

def print_mu(mu, mode=PrintMode.Symbolica, **kwargs):
    if mode != PrintMode.Latex:
        return None

    if mu.get_type() == AtomType.Fn:
        return r'\mu_{' + ','.join(a.format() for a in mu) + '}'

    return r'\mu'

mu = S('mu', print=print_mu)
print((mu + mu(1, 2)).to_latex())
use symbolica::prelude::*;

fn main() {
    let _mu = symbol!("mu", print = |a, opt, _state| {
        if !opt.mode.is_latex() {
            return None;
        }

        let mut fmt = "\\mu".to_string();
        if let AtomView::Fun(f) = a {
            fmt.push_str("_{");
            let n_args = f.get_nargs();
            for (i, arg) in f.iter().enumerate() {
                arg.format(&mut fmt, opt, PrintState::new()).unwrap();
                if i < n_args - 1 {
                    fmt.push_str(",");
                }
            }
            fmt.push('}');
        }

        Some(fmt)
    });

    println!("{}", parse!("mu + mu(1,2)").printer(PrintOptions::latex()));
}

Output

\mu+\mu_{1,2}

Derivatives

A derivative hook defines how a function differentiates with respect to one of its arguments. The callback receives the function expression and the zero-based argument index being differentiated.

from symbolica import *

tag = S('tag', derivative=lambda f, index: f)
x = S('x')

print(tag(3, x).derivative(x))
use symbolica::prelude::*;

fn main() {
    let _tag = symbol!("tag", der = |a, _arg, out| {
        out.set_from_view(&a);
    });

    println!("{}", parse!("tag(3,x)").derivative(symbol!("x")));
}

Output

tag(3,x)

Series

A series hook can override the series expansion of a function near a singular point. It receives the already expanded argument series and may return the singular factor and regularized expression. Return None to use the default construction.

from symbolica import *

def inv_series(args):
    return (N(0), args[0].pow(-1).to_expression())

t = S('t')
inv = S('inv', series=inv_series)

print(inv(1/t).series(t, 0, 0).to_expression())
use symbolica::prelude::*;

fn main() {
    let _inv = symbol!("inv", series = |args| {
        Some((Atom::Zero, args[0].rpow((-1).into()).unwrap().to_atom()))
    });

    let s = parse!("inv(1/t)").series(symbol!("t"), 0, 0).unwrap();
    println!("{}", s.to_atom());
}

Output

0

Numeric Evaluation

An evaluation hook registers numerical implementations for a function. This lets evaluators call the external implementation without rewriting the expression symbolically.

from symbolica import *

double = S(
    'double',
    eval={
        'float': lambda args: 2.0 * args[0],
        'complex': lambda args: 2.0 * args[0],
    },
)
x = S('x')

print(double(x).evaluate({x: 3.0}))
use symbolica::prelude::*;

fn main() {
    let _double = symbol!(
        "double",
        eval = EvaluationInfo::new().register(|args: &[f64]| 2.0 * args[0])
    );

    let params = vec![parse!("x")];
    let mut evaluator = parse!("double(x)")
        .evaluator(&params)
        .build()
        .unwrap()
        .map_coeff(&|x| x.re.to_f64());

    println!("{}", evaluator.evaluate_single(&[3.0]));
}

Output

6

Constants

Constants do not have numeric arguments from which an evaluator can infer a precision. Register them with a precision-aware constant callback instead. In Python, eval['constant'] is called with an empty argument list and a requested decimal precision for tagless constants. In Rust, EvaluationInfo::constant receives the symbolic tags and the requested binary precision.

from decimal import Decimal, localcontext
from symbolica import *

def third_constant(args, prec):
    with localcontext() as ctx:
        ctx.prec = prec
        return Decimal(1) / Decimal(3)

third = S('third', eval={'constant': third_constant})
print(third.to_float(30))
use symbolica::prelude::*;

fn main() {
    let _third = symbol!(
        "third",
        eval = EvaluationInfo::constant(|_tags, prec| {
            Ok((Float::with_val(prec, 1) / Float::with_val(prec, 3)).into())
        })
    );

    println!("{}", parse!("third").to_float(30));
}

Output

3.33333333333333333333333333333e-1

eval['constant'] cannot be combined with float, complex, decimal, or decimal_complex callbacks. Use it for objects such as mathematical constants whose value depends on the requested precision rather than on numeric arguments.

User Data

User data attaches structured metadata to a symbol. Symbolica does not interpret it automatically, but library code can retrieve it later.

from symbolica import *

external_x = S('external_x', data={'source': 'input'})

print(external_x.get_symbol_data('source'))
use symbolica::prelude::*;

fn main() {
    let external_x = symbol!(
        "external_x",
        data = UserData::String("input".to_owned())
    );

    println!("{:?}", external_x.get_data());
}

Output

Python:
input

Rust:
String("input")

In Rust, combine attributes with custom settings by separating the attributes from settings with a second semicolon:

use symbolica::prelude::*;

let _dot = symbol!("dot"; Symmetric, Linear; tags = [tag!("bilinear")]);

Advanced usage

Register library symbols

Library authors often need to define symbols with attributes, aliases, hooks, or data before user code starts parsing expressions. In Rust, use the initialize! macro at module scope to register those symbols when Symbolica’s global state is initialized.

use symbolica::prelude::*;

initialize!(|| {
    symbol!("my_library::dot"; Symmetric, Linear);
    symbol!(
        "my_library::third",
        eval = EvaluationInfo::constant(|_tags, prec| {
            Ok((Float::with_val(prec, 1) / Float::with_val(prec, 3)).into())
        })
    );
});

The initializer is intended for crates that expose Symbolica symbols as part of their public API. It avoids relying on users to call a setup function before parsing expressions that use those symbols.

If one library initializer depends on symbols registered by another crate, list the dependency crate names after the closure:

use symbolica::prelude::*;

initialize!(|| {
    symbol!("my_library::f"; Linear);
}, "other_symbolica_library");