Common features

Symbolica documentation for getting started, symbolic expressions, numerical evaluation, pattern matching, and APIs in Python and Rust.

Below we list some common features of Symbolica.

Expanding

Symbolica can expand an expression:

from symbolica import Expression
x = Expression.symbol('x')
e = (1+x)^2
print(e.expand())
use symbolica::atom::{Atom, AtomCore};

fn main() {
    let e = parse!("(1+x)^2").unwrap();
    println!("{}", e.expand(None));
}

Output

x^2+2x+1

Expansions can also be performed in a particular variable:

from symbolica import Expression
x, y = Expression.symbol('x', 'y')
e = (1+y)^100 + (1+x)^2
print(e.expand(x))
use symbolica::atom::{Atom, AtomCore, Symbol};

fn main() {
    let x = symbol!("x");
    let e = parse!("(1+y)^100 + (1+x)^2").unwrap();
    println!("{}", e.expand(Some(x)));
}

Output

x^2+2x+1 + (1+y)^100

Collecting

A coefficient list with respect to one or more variables can be obtained:

from symbolica import Expression
x, y = Expression.symbol('x', 'y')
e = 5*x + x * y + x**2 + 5

l = e.coefficient_list(x)
for key, value in l:
    print(key, value)
use symbolica::atom::{Atom, AtomCore};

fn main() {
    let x_symb = symbol!("x");
    let e = parse!("5*x + x * y + x**2 + 5").unwrap();

    for (k, v) in e.coefficient_list(x_symb).0 {
        println!("{}, {}", k, v);
    }
}

Output

x^2 1
x y+5
1 5

You can also collect in one or multiple variables:

from symbolica import Expression
x, y = Expression.symbol('x', 'y')
e = 5*x + x * y + x**2 + 5

print(e.collect(x))
use symbolica::atom::{Atom, AtomCore};

fn main() {
    let x_symb = symbol!("x");
    let e = parse!("5*x + x * y + x**2 + 5").unwrap();
    println!("{}", e.collect(x_symb, None, None));
}

Output

x^2+x*(y+5)+5

You can also provide a user-defined function to map the key and one to map the coefficient. For example:

from symbolica import Expression

x, y = Expression.symbol('x', 'y')
var, coeff = Expression.symbol('var', 'coeff')

e = 5*x + x * y + x**2 + 5

e = e.collect(x, key_map=lambda x: symbol(x), coeff_map=lambda x: coeff(x))
print(e)
use symbolica::{
    atom::AtomCore, function, parse, symbol
};

fn main() {
    let input = parse!("x*(1+a)+x*5*y+f(5,x)+2+y^2+x^2 + x^3").unwrap();
    let x = symbol!("x");
    let key = symbol!("key");
    let coeff = symbol!("coeff");

    println!("> Collect in x with wrapping:");
    let out = input.collect(
        x,
        Some(Box::new(move |a, out| {
            out.set_from_view(&a);
            *out = function!(key, out);
        })),
        Some(Box::new(move |a, out| {
            out.set_from_view(&a);
            *out = function!(coeff, out);
        })),
    );
    println!("\t{}", out);
}

Output

symbol(1)*coeff(5)+symbol(x)*coeff(y+5)+symbol(x^2)*coeff(1)

Together

To write an expression using a common denominator, use together:

from symbolica import Expression
x, y, z = Expression.symbol('x', 'y', 'z')
e = (1/x + (2*x+y+z)/y).together()
print(e)
use symbolica::{atom::Atom};

fn main() {
    let input = parse!("1/x + (2*x+y+z)/y").unwrap();
    println!("{}", input.together());
}

Output

(x*y)^-1*(y+2*x^2+x*y+x*z)

Apart

To apply partial fraction decomposition in a variable x, use apart(x):

from symbolica import Expression
x, y, z = Expression.symbol('x', 'y', 'z')
e = ((y+2*x**2+x*y+x*z)/(x*y)).apart(x)
print(e)
use symbolica::{atom::AtomCore, parse};

fn main() {
    let input = parse!("(y+2*x^2+x*y+x*z)/(x*y)").unwrap();
    println!("{}", input.apart(symbol!("x")));
}

Output

x^-1+y^-1*(2*x+y+z)

Cancel

Symbolica can cancel common factors between numerators and denominators, leaving all other parts of the expression untouched:

from symbolica import *
p = Expression.parse('1+(y+1)^10*(x+1)/(x^2+2x+1)')
print(p.cancel())
use symbolica::atom::{Atom, AtomCore};

fn main() {
    let a = parse!("1+(y+1)^10*(x+1)/(x^2+2x+1)").unwrap();
    println!("{}", a.cancel());
}

Output

1+(y+1)**10/(x+1)

Derivatives

Symbolica can derive expressions with built-in and user-defined functions:

from symbolica import Expression
x = Expression.symbol('x')

print(Expression.parse("(1+2*x)^(5+x)").derivative(x))
print(Expression.parse("log(2*x) + exp(3*x) + sin(4*x) + cos(y*x)").derivative(x))
print(Expression.parse("f(x^2,x)").derivative(x))
print(Expression.parse("der(0,1,f(x,x^3))").derivative(x))
use symbolica::{atom::AtomCore, parse, symbol};

fn main() {
    let x = symbol!("x");
    let inputs = [
        "(1+2*x)^(5+x)",
        "log(2*x) + exp(3*x) + sin(4*x) + cos(y*x)",
        "f(x^2,x)",
        "der(0,1,f(x,x^3))",
    ];

    for input in inputs {
        let input = parse!(input).unwrap();

        let a = input.derivative(x);

        println!("d({})/dx = {}:", input, a);
    }
}

Output

(2*x+1)^(x+5)*log(2*x+1)+2*(x+5)*(2*x+1)^(x+4)
2*(2*x)^-1+3*exp(3*x)+4*cos(4*x)-y*sin(x*y)
der(0,1,f(x^2,x))+2*x*der(1,0,f(x^2,x))
der(1,1,f(x,x^3))+3*x^2*der(0,2,f(x,x^3))

The built-in der function keeps counters of the number of derivatives per argument position.

You can define custom derivatives for your symbols:

x = S('x')
tan = S('tan', custom_derivative=lambda f, var: f * S('sec')(f[var])**2)
sec = S('sec', custom_derivative=lambda f, var: f * S('tan')(f[var]))

e = (sec(x**2 + 1) / tan(x)).derivative(x)
print(e.factor())

Output

-(sec(x)^2-2*x*tan(x^2+1))*tan(x)^-1*sec(x^2+1)

Series expansion

Symbolica can also produce a Puiseux series, with additional support for log(x) around x=0. The function series returns a series object that support efficient arithmetic with other series.

from symbolica import Expression
x = Expression.symbol('x')
e = Expression.parse('exp(5+x)/(1-x)').series(x, 0, 3)
use symbolica::{atom::AtomCore, parse, symbol};

fn main() {
    let x = symbol!("x");
    let a = parse!("exp(5+x)/(1-x)").unwrap();
    let out = a.series(x, Atom::new().as_view(), 3);

    println!("{}", out);
}

Output

(exp(5))+(2*exp(5))*x+(5/2*exp(5))*x^2+(8/3*exp(5))*x^3+O(x^4)

Factorization

Symbolica can factor an expression:

from symbolica import *
e = Expression.parse('(6 + x)/(7776 + 6480*x + 2160*x^2 + 360*x^3 + 30*x^4 + x^5)')
e.factor()
use symbolica::atom::{Atom, AtomCore};

fn main() {
    let a = parse!("(6 + x)/(7776 + 6480*x + 2160*x^2 + 360*x^3 + 30*x^4 + x^5)").unwrap();
    println!("{}", a.factor());
}

Output

(x+6)^-4

Solving linear system

Symbolica can solve linear systems:

from symbolica import Expression

x, y, c = Expression.symbol('x', 'y', 'c')
f = Expression.symbol('f')

x_r, y_r = Expression.solve_linear_system(
    [f(c)*x + y + c, y + c**2], [x, y])
print('x =', x_r, ', y =', y_r)

Output

x = (-c+c^2)*f(c)^-1 , y = -c^2
use symbolica::atom::{Atom, AtomView};

fn main() {
    let x = symbol!("x");
    let y = symbol!("y");
    let eqs = ["f(c)*x + y + c", "y + c**2"];

    let atoms: Vec<_> = eqs.iter().map(|e| parse!(e).unwrap()).collect();
    let system: Vec<_> = atoms.iter().map(|x| x.as_view()).collect();

    let sol = AtomView::solve_linear_system::<u8>(&system, &[x, y]).unwrap();

    for (v, s) in ["x", "y"].iter().zip(&sol) {
        println!("{} = {}", v, s);
    }
}

Numerically solving non-linear system

Symbolica can use Newton’s method to find roots of expressions:

from symbolica import *
a = E("x^2-2").nsolve(E("x"), 1.) # using float

b = E("x^2-2").nsolve(E("x"),
        Decimal("1.000000000000000000000000000000000000000000000000000000000000000000000000"),
        1e-74, 1000000)
use symbolica::atom::{Atom, AtomView};

fn find_root() {
    let x = symbol!("x");
    let a = parse!("x^2 - 2").unwrap();
    let a = a.as_view();

    let root = a.nsolve(x, 1.0, 1e-10, 1000).unwrap();
    assert!((root - 2f64.sqrt()).abs() < 1e-10);
}
 1.414213562373095048801688724209698078569671875376948073176679737990732478

Or it can solve systems:

\[ \begin{align} 5x^2+x y^2+\sin(2y)^2 - 2 &= 0\\ \exp(2x-y)+4y - 3 &= 0 \end{align} \]

from symbolica import *
a = Expression.nsolve_system([E("5x^2+x*y^2+sin(2y)^2 - 2"), E("exp(2x-y)+4y - 3")],
        [S("x"), S("y")], [1., 1.], 1e-9)
use symbolica::atom::{Atom, AtomView};

fn solve_system_newton() {
    let a = parse!("5x^2+x*y^2+sin(2y)^2 - 2").unwrap();
    let b = parse!("exp(2x-y)+4y - 3").unwrap();

    let r = AtomView::nsolve_system(
        &[a.as_view(), b.as_view()],
        &[symbol!("x"), symbol!("y")],
        &[F64::from(1.), F64::from(1.)],
        F64::from(1e-10),
        100,
    )
    .unwrap();
}
[0.5672973499396123, -0.30944227920271095]

Canonizing tensors

It is often convenient to detect if terms with tensors are equivalent, for example:

e1 = g(mu2, mu3)*fc(mu4, mu2, k1, mu4, k1, mu3)

and

e2 = g(mu3, mu2)*fc(k1, mu1, k1, mu2, mu1, mu3)

If they are isomorphic, we can merge the two terms in e1+e2 into one and prevent double work.

Symbolica can canonize tensors with dummy/contracted indices:

from symbolica import *
g = Expression.symbol('g', is_symmetric=True)
fc = Expression.symbol('fc', is_cyclesymmetric=True)
mu1, mu2, mu3, mu4, k1 = Expression.symbol('mu1', 'mu2', 'mu3', 'mu4', 'k1')

e = g(mu2, mu3)*fc(mu4, mu2, k1, mu4, k1, mu3)
print(e.canonize_tensors([mu1, mu2, mu3, mu4]))
use symbolica::{atom::AtomCore, parse, symbol};

fn main() {
    let _ = symbol!("g"; Symmetric).unwrap();
    let _ = symbol!("fc"; Cyclesymmetric).unwrap();
    let a = parse!("g(mu2,mu3)*fc(mu4,mu2,k1,mu4,k1,mu3)").unwrap();
    let mu1 = parse!("mu1").unwrap();
    let mu2 = parse!("mu2").unwrap();
    let mu3 = parse!("mu3").unwrap();
    let mu4 = parse!("mu4").unwrap();
    let r = a.canonize_tensors(&[mu1.as_view(), mu2.as_view(), mu3.as_view(), mu4.as_view()], None).unwrap();
    println!("{}", r);
}

Output

g(mu1,mu2)*fc(mu1,mu3,mu2,k1,mu3,k1)

Anti-symmetric tensors are supported as well. For example, the canonization turns

f(mu3,mu2,mu3,mu1)*f_anti(mu1,mu2)

into

Output

-f(mu1,mu2,mu1,mu3)*f_anti(mu2,mu3)