Expressions
Representation
A mathematical expression in Symbolica can consist of rational numbers, variables, functions, and the operations addition, multiplication, and exponentiation. An example Symbolica expression is:
\[ f(x,y)^2 + x \cdot y + 1\]
Division and negation are represented through a power of -1 and a multiplication by -1 respectively:
\[ \frac{1}{x} \rightarrow x^{-1},\qquad -x = -1*x \]
The addition and multiplication operator are viewed as n-ary operators, instead of binary operators. What that means is all factors or summands are treated at the same level. In a tree representation it looks like:
We can represent any Symbolica expression as a tree, where we refer to each node as an atom. For example, the expression
\[\frac{4}{3} (x+1) f(x,y^z,3)\] can be viewed as:
Note that although we can represent the expression by a tree, this is not the way Symbolica represents, them as this would take too much memory. Symbolica uses a custom linear, compressed expression that is approximately 8 times smaller than Mathematica’s and 3 times smaller than Form’s. This way, many terms can fit in memory at the same time. See Memory Representation for more details.
Even though Symbolica does not (yet) natively support vectors or other mathematical constructs you may want to use, functions are flexible enough to capture these construct. For example, a vector with an index could be represented as vec(p,mu)
, representing \(p^\mu\), a dot product \(p_1 \cdot p_2\) could be represented as dot(p1,p2)
, etc.
Creating Symbolica expressions
In the following example we create a Symbolica expression f(x)*(1+y)
via string parsing:
from symbolica import Expression
= Expression.parse('f(x)*(1+y)')
e print(e)
use symbolica::atom::Atom;
fn main() {
let input = Atom::parse("f(x)*(1+y)").unwrap();
println!("{}", input);
}
The parsing rules are liberal. Whitespace characters (space, tab, newline) are allowed to appear between tokens. A variable name and function name must start with a non-numerical unicode character and may be followed by any non-whitespace character. Functions may have parentheses or square brackets. A number must consist of numerical characters (0
-9
) and may contain digit separators _
(underscore) or
(thin space, U+2009). The multiplication operator may be implicit, which means that it may be inserted when applicable. Here are some examples of valid input:
f[5,y] + f(x)
2x
3(2+x)
x^2y
2 * 3_000_123
x2 y
Whitespace between two numbers (for example 5 6
or 5\n6
) will result in a parsing error instead of an implicit multiplication (5*6
), as it is very likely that the user intended this to be one number (56
).
Floating point numbers
Floating point numbers can be parsed in decimal notation or scientific notation:
1.3
1.4e-4
The precision of a floating point – the number of precise decimal digits - can be specified by appending it after a backtick to the float. For example 5.6e-2`1.2
, denotes that the float 5.6e-2
has 1.2
significant decimal digits.
Floating point numbers without an explicit precision are assumed to have at least the same precision as 64-bit double floating point numbers (53 binary digits, or 15.9 decimal digits). During computations in Symbolica, the precision of floating points is tracked. For more advanced precision tracking, see the ErrorPropagatingFloat
type.
Using Python’s decimal
class, you can parse a specific number of stable digits, e.g. Decimal('0.01234')
has 4 stable decimal digits.
Manual construction
Atoms / expressions can also be constructed from combinations of variables, functions and numbers:
from symbolica import Expression
= Expression.symbol('x', 'y', 'f')
x, y, f = f(x)*(1+y)
e print(e)
use the fun!
macro to create Symbolica functions and the symb!
macro to quickly define symbols:
use symbolica::{
,
funatom::{Atom, FunctionBuilder},
state::State,
};
fn main() -> Result<(), String> {
let (x, y, f_id) = symb!("x", "y", "f");
let f = fun!(f_id, x, y, 2);
let xb = (-(y + x + 2) * y * 6).npow(5) / y * &f / 4;
println!("{}", xb);
Ok(())
}
In Python you can use S
instead of Expression.symbol
, E
instead of Expression.parse
and N
instead of Expression.num
!
Symbol attributes
Symbols can have special properties that alters their behaviour. Most of these properties apply when the symbol is used as a function. For example, a symbol can be (anti)symmetric. This has the effect that the function arguments will be sorted according to the canonical form. For example, f(3,2,1)
where f
is symmetric:
from symbolica import Expression
= Expression.symbol('f', is_symmetric=True)
f = f(3,2,1)
e print(e)
use symbolica::{
,
funatom::{Atom, FunctionBuilder},
state::{FunctionAttribute, State},
};
fn main() -> Result<(), String> {
let f = State::get_symbol_with_attributes("f", &[FunctionAttribute::Symmetric])?;
let a = fun!(
,
fAtom::new_num(3),
Atom::new_num(2),
Atom::new_num(1)
;
)
println!("{}", a);
Ok(())
}
yields f(1,2,3)
. Antisymmetric functions will also be sorted and will obtain a minus sign for every swapped argument.
Symbols can also be defined as linear, which will linearize every argument. Combining the attributes symmetric and linear gives the familiar dot product:
from symbolica import Expression
= Expression.symbol('f', is_symmetric=True, is_linear=True)
f = Expression.parse("f(3*x+y,x+2*y+z)")
e print(e)
use symbolica::{
atom::Atom,
state::{FunctionAttribute, State},
};
fn main() {
let _f = State::get_symbol_with_attributes("f", &[FunctionAttribute::Linear, FunctionAttribute::Symmetric]);
let out = Atom::parse("f(3*x+y,x+2*y+z)").unwrap();
println!("{}", out);
}
which yields f(x,y)+3*f(x,z)+f(y,z)+3*f(x,x)+2*f(y,y)+6*f(x,y)
.
Once symbols have been defined, their attributes cannot be changed anymore as this would invalidate previously built expressions. When parsing an expression, the default attributes will be assigned to previously undefined symbols. Therefore, make sure to define symbols with special attributes before parsing any expressions.
Walking the expression tree
The expression tree can be navigated by looping through the nodes and leafs:
from symbolica import *
def walk_tree(a: Expression, depth: int = 0):
if a.get_type() in [AtomType.Var, AtomType.Num]:
print(' '*depth, a)
if a.get_type() == AtomType.Fn:
print(' '*depth, 'Fun', a.get_name())
for arg in a:
+ 1)
walk_tree(arg, depth if a.get_type() == AtomType.Mul:
print(' '*depth, 'Mul')
for arg in a:
+ 1)
walk_tree(arg, depth if a.get_type() == AtomType.Add:
print(' '*depth, 'Add')
for arg in a:
+ 1)
walk_tree(arg, depth if a.get_type() == AtomType.Pow:
print(' '*depth, 'Pow')
0], depth + 1)
walk_tree(a[1], depth + 1)
walk_tree(a[
= Expression.parse('x^2*y + f(1,2,3)')
e walk_tree(e)
use symbolica::{
id::AtomTreeIterator,
atom::{Atom, AtomView},
};
fn tree_walk(a: AtomView, depth: usize) {
match a {
AtomView::Num(_) | AtomView::symbol(_) => println!("{:indent$}{}", "", a, indent = depth),
AtomView::symbol(f) => {
println!("{:indent$}Fun {}", "", f.get_symbol(), indent = depth);
for a in f.iter() {
, depth + 1);
tree_walk(a}
}
AtomView::Pow(p) => {
println!("{:indent$}", " ", indent = depth);
let (base, exp) = p.get_base_exp();
, depth + 1);
tree_walk(base, depth + 1);
tree_walk(exp}
AtomView::Mul(m) => {
println!("{:indent$}Mul", "", indent = depth);
for a in m.iter() {
, depth + 1);
tree_walk(a}
}
AtomView::Add(a) => {
println!("{:indent$}Add", "", indent = depth);
for a in a.iter() {
, depth + 1);
tree_walk(a}
}
}
}
fn main() {
let expr: Atom = Atom::parse("x^2*y + f(1,2,3)").unwrap();
.as_view(), 0);
tree_walk(expr}
which yields:
Add
Fun f
1
2
3
Mul
Pow
x
2
y
To modify parts of the expression, use pattern matching with replacement.
Output formatting
Symbolica can format its output in a customizable way. For example, it can write Mathematica-compatible output or Latex output. Here is an example:
from symbolica import Expression
= Expression.parse('128378127123 z^(2/3)*w^2/x/y + y^4 + z^34 + x^(x+2)+3/5+f(x,x^2)')
a print(a.pretty_str(multiplication_operator=' ', num_exp_as_superscript=True, number_thousands_separator='_'))
print(a.pretty_str(latex=True))
use symbolica::{printer::AtomPrinter, atom::Atom};
fn main() {
let n =
Atom::parse("128378127123 z^(2/3)*w^2/x/y + y^4 + z^34 + x^(x+2)+3/5+f(x,x^2)").unwrap();
let mut p = AtomPrinter::new(n.as_view());
.print_opts.number_thousands_separator = Some('_');
p.print_opts.multiplication_operator = ' ';
p.print_opts.num_exp_as_superscript = true;
pprintln!("{}", p);
let mut p = AtomPrinter::new(n.as_view());
.print_opts.latex = true;
pprintln!("{}", p);
}
which yields:
z³⁴+x^(x+2)+y⁴+f(x,x²)+128_378_127_123 z^(2/3) w² x⁻¹ y⁻¹+3/5
$$z^{34}+x^{x+2}+y^{4}+f(x,x^{2})+128378127123 z^{\frac{2}{3}} w^{2} \frac{1}{x} \frac{1}{y}+\frac{3}{5}$$
Canonical form
An expression has a normal form or canonical form, which is one preferred way in which it is written. For example, the normal form of 1+1
is 2
, and the normal form of x*x
is x^2
. The procedure of normalizing an expression is performed after every operation, so that you should never see a non-canonical expression. Each computer algebra system has its own rules to decide which form is the normal form (or may not even write expression in a unique form!).
The (recursive) rules for normalization for Symbolica are defined per atom. For a
- Variable
- Always normalized
- Number
- Remove the common divisor between the numerator and denominator
- Sum
- Normalize and sort all summands
- Remove summands that are 0
- Add summands that are equal apart from their coefficient (\(x+ 2x \rightarrow 3x\))
- Product
- Normalize and sort all factors
- If any factor is 0, return the number 0
- Remove factor of 1
- Convert equal factors apart from their power into a power (\(x x^a \rightarrow x^{a+1}\))
- Power
- Normalize the base and exponent
- Normalize \(\text{num}^\text{num}\), \(0^a \rightarrow 1\), and \(1^a \rightarrow 1\)
- Function
- Normalize all arguments
- Flatten arguments of built-in function
arg(...)
- Sort all arguments when the function is symmetric
The comparison of two atoms, which is used for sorting, is well-defined, however the convention could change between Symbolica versions.
Note that normalization is supposed to be quick, since it happens often. This means that expensive operations, such as expansion and factorization are not considered. For example, \((x+1)^2\) is a Symbolica normal form and \(1+2x+x^2\) is as well, even though they are mathematically equivalent.