flowchart TD
Mul --> Num[Num 4/3]
Mul --> Add
Add --> Varx[Var x]
Add --> Num2[Num 1]
Mul --> Fn[Fn f]
Fn --> Varxx[Var x]
Fn --> Pow[Pow]
Pow --> Vary[Var y]
Pow --> Varz[Var z]
Fn --> Num3[Num 3]
Expressions
Expressions are the central values in Symbolica. They are what you parse, build, transform, match, differentiate, evaluate, and print.
The important user model is:
- An expression is built from numbers, variables, functions, sums, products, and powers. Integers and rationals are exact; floating-point numbers can also appear when you parse or construct approximate values.
- Every expression is normalized as it is created, so simple algebraic identities such as
1+1andx*xare represented as2andx^2. - Functions are generic symbolic heads. You can use them for mathematical functions such as
sin(x), but also to model domain objects such asvec(p,mu)ordot(p1,p2). - Symbols control names, namespaces, attributes, tags, aliases, and custom behavior. See Symbols for those details.
This page focuses on the expression object itself. For common algebraic operations such as expansion, replacement, differentiation, and factorization, see First steps.
Create Expressions
You can create expressions by parsing text, or by building them from symbols and arithmetic operators. Parsing is compact and convenient for user input. Programmatic construction is useful when your code already has the pieces of the expression.
from symbolica import *
x, y, f = S('x', 'y', 'f')
expr = (x + 1)**2 + f(y, x) / 3
print(expr)use symbolica::prelude::*;
fn main() {
let (x, y, f) = symbol!("x", "y", "f");
let expr = (x + 1).npow(2) + function!(f, y, x) / 3;
println!("{expr}");
}Output
1/3*f(y,x)+(1+x)^2
In Python, S, E, N, and T are shorthands for Expression.symbol, Expression.parse, Expression.num, and Transformer.
Parse Input
The parser accepts common mathematical syntax and normalizes the expression immediately.
from symbolica import *
expr = E('f(x)*(1+y) + x*x + 1 + 1')
print(expr)use symbolica::prelude::*;
fn main() {
let expr = parse!("f(x)*(1+y) + x*x + 1 + 1");
println!("{expr}");
}Output
2+(1+y)*f(x)+x^2
Use Mathematica parsing mode for expressions written in Mathematica InputForm, such as square-bracket function calls and backtick namespaces. Symbolica supports a useful subset of InputForm; for maximal compatibility, export from Mathematica with NumberMarks -> False, or use FullForm for structures that do not have direct Symbolica syntax.
from symbolica import *
expr = E('Cos[test`x] (test`a + test`b)^2', mode=ParseMode.Mathematica)
print(expr.format(
show_namespaces=True,
multiplication_operator='*',
num_exp_as_superscript=False,
))
print(expr.to_mathematica())use symbolica::prelude::*;
fn main() {
let expr = parse!("Cos[test`x] (test`a + test`b)^2", Mathematica);
println!("{expr:#}");
println!("{}", expr.printer(PrintOptions::mathematica()));
}Output
(test::a+test::b)^2*cos(test::x)
(test`a+test`b)^2 Cos[test`x]
Useful parser rules:
- Whitespace is allowed between tokens.
- Multiplication may be implicit:
2x,3(2+x), andx^2y. - Functions may use parentheses or square brackets:
f(x)andf[x]. - Names must start with a non-numerical Unicode character.
- Integer digits may contain separators such as
_or thin spaces:3_000_123. - Whitespace between two numbers, such as
5 6, is an error rather than implicit multiplication.
Imaginary units are parsed by suffixing a number with i or 𝑖, for example 2+3i. The suffix binds to the closest number, so 2/3i is read as 2/(3i).
Format Output
In Python notebooks, expressions have an HTML representation by default, so evaluating an expression as the last value in a Jupyter cell gives a rich display automatically. For custom formatted notebook output, use formatted.
Symbolica printing can be customized in many ways, including using superscripts, LaTeX output, Typst output, and writing large output in a line-wrapped, readable way.
from symbolica import *
expr = E('128378127123*z^(2/3)*w^2/x/y + y^4 + z^34 + x^(x+2) + 3/5 + f(x,x^2)')
print(expr.format(
multiplication_operator=' ',
num_exp_as_superscript=True,
number_thousands_separator='_',
))
print(expr.to_latex())use symbolica::prelude::*;
fn main() {
let expr =
parse!("128378127123*z^(2/3)*w^2/x/y + y^4 + z^34 + x^(x+2) + 3/5 + f(x,x^2)");
println!("{}", expr.printer(&PrintOptions {
number_thousands_separator: Some('_'),
multiplication_operator: ' ',
num_exp_as_superscript: true,
..PrintOptions::default()
}));
println!("{}", expr.printer(PrintOptions::latex()));
}Output
3/5+f(x,x²)+z³⁴+128_378_127_123 z^(2/3) w²/(x y)+x^(2+x)+y⁴
$$\frac{3}{5}+f\!\left(x,x^{2}\right)+z^{34}+\frac{128378127123 z^{\frac{2}{3}} w^{2}}{x y}+x^{2+x}+y^{4}$$
Numbers
Integers and rational numbers are exact. Division of exact integers creates exact rational numbers, not floating-point approximations.
Floating-point numbers can be parsed in decimal or scientific notation. The precision of a floating-point number can be specified after a backtick. For example, 5.6789e-2`10 denotes a number with 10 significant decimal digits.
from symbolica import *
print(E('1/3 + 2/3 + 1.4e-4').format_plain())
print(E('1.4e-4 + 5.6789e-2`10').format_plain())use symbolica::prelude::*;
fn main() {
println!("{}", parse!("1/3 + 2/3 + 1.4e-4"));
println!("{}", parse!("1.4e-4 + 5.6789e-2`10"));
}Output
1.0001400000000000000
5.692900000e-2
Floating-point numbers without explicit precision are assumed to have at least the precision of a 64-bit double, which is 53 binary digits or about 15.9 decimal digits. During computations, Symbolica tracks this precision. In Python, you can also parse stable decimal digits through Python’s decimal.Decimal, for example Decimal('0.01234').
For numerical approximations of symbolic expressions, see Numerical evaluation.
Inspect Expressions
Most expression transformations should use high-level operations or pattern matching. When you need custom analysis, you can inspect the expression tree directly.
from symbolica import *
def line(depth: int, text: str):
print(' '*depth + text)
def walk_tree(expr: Expression, depth: int = 0):
if expr.get_type() in [AtomType.Var, AtomType.Num]:
line(depth, str(expr))
elif expr.get_type() == AtomType.Fn:
name = expr.get_name().split('::')[-1]
line(depth, f'Fun {name}')
for arg in expr:
walk_tree(arg, depth + 1)
elif expr.get_type() == AtomType.Mul:
line(depth, 'Mul')
for arg in expr:
walk_tree(arg, depth + 1)
elif expr.get_type() == AtomType.Add:
line(depth, 'Add')
for arg in expr:
walk_tree(arg, depth + 1)
elif expr.get_type() == AtomType.Pow:
line(depth, 'Pow')
walk_tree(expr[0], depth + 1)
walk_tree(expr[1], depth + 1)
walk_tree(E('x^2*y + f(1,2,3)'))use symbolica::prelude::*;
fn walk_tree(expr: AtomView, depth: usize) {
match expr {
AtomView::Num(_) | AtomView::Var(_) => println!("{:indent$}{}", "", expr, indent = depth),
AtomView::Fun(f) => {
println!("{:indent$}Fun {}", "", f.get_symbol(), indent = depth);
for arg in f.iter() {
walk_tree(arg, depth + 1);
}
}
AtomView::Pow(p) => {
println!("{:indent$}Pow", "", indent = depth);
let (base, exp) = p.get_base_exp();
walk_tree(base, depth + 1);
walk_tree(exp, depth + 1);
}
AtomView::Mul(m) => {
println!("{:indent$}Mul", "", indent = depth);
for arg in m.iter() {
walk_tree(arg, depth + 1);
}
}
AtomView::Add(a) => {
println!("{:indent$}Add", "", indent = depth);
for arg in a.iter() {
walk_tree(arg, depth + 1);
}
}
}
}
fn main() {
let expr = parse!("x^2*y + f(1,2,3)");
walk_tree(expr.as_view(), 0);
}Output
Add
Fun f
1
2
3
Mul
Pow
x
2
y
Representation
At the user level, expressions behave like trees. Each node is called an atom. The main atom kinds are numbers, variables, functions, sums, products, and powers.
Division and negation are represented using powers and multiplication:
\[ \frac{1}{x} \rightarrow x^{-1},\qquad -x = -1*x \]
Addition and multiplication are n-ary rather than binary. In other words, all summands or factors live at the same level:
flowchart TD
Mul --> Mulb[Mul]
Mul --> a
Mulb --> b
Mulb --> c
flowchart TD
Mul --> a
Mul --> b
Mul --> c
For example, the expression
\[\frac{4}{3} (x+1) f(x,y^z,3)\]
can be viewed as:
Symbolica does not store expressions as pointer-heavy trees internally. It uses a custom linear, compressed representation so that many more terms fit in memory.
Canonical Form
An expression has a normal form, or canonical form, which is the preferred way Symbolica writes it. For example, the normal form of 1+1 is 2, and the normal form of x*x is x^2.
Normalization happens after every operation, so user code should not see partially normalized expressions. The recursive normalization rules are defined per atom:
- Variables are already normalized.
- Numbers are reduced by removing common factors from numerator and denominator.
- Sums normalize and sort all summands, remove zero summands, and combine equal summands apart from their coefficient.
- Products normalize and sort all factors, return zero if any factor is zero, remove factors of one, and combine equal factors into powers.
- Powers normalize the base and exponent and simplify numerical powers,
0^a, and1^a. - Functions normalize all arguments, flatten arguments of built-in
arg(...), and sort arguments when the function is symmetric.
The comparison used for sorting atoms is well-defined, but the exact ordering convention may change between Symbolica versions.
Normalization is intentionally fast because it happens often. Expensive operations such as expansion and factorization are not part of canonicalization. For example, both (x+1)^2 and 1+2*x+x^2 are normal forms, even though they are mathematically equivalent.