Introduction
Symbolica is a high-performance symbolic computation framework for Python and Rust. You can use it to manipulate symbolic expressions and turn them into fast numerical kernels for computing Jacobians, numerical optimization, integration, and much more.
Today marks the 2.2 release of Symbolica. It’s new exciting feature is symbolic integration, which allows you to integrate expressions symbolically and get a step-by-step overview of the integration process.
Symbolic integration
Differentiation is a common operation and is straightforward to implement. The inverse, anti-derivation, is much more difficult. In this section we will explore why this is the case.
A derivative operation on an expression can be cast into a pattern matching rule, by virtue of the chain rule. The chain rule says that:
\[ \frac{d}{dx} f(g(x)) = f'(g(x)) \cdot g'(x) \]
which decouples the derivative of the argument of the function from the derivative of the function itself. For example, the derivative of sin(f(x)) is cos(f(x))*f'(x). This means that we can implement differentiation recursively by pattern matching on the expression and applying the appropriate rules. For example, below the derivative operation is implemented for a simple expression tree in Rust:
struct Expression {
Num(isize),
Var(usize),
Add(Box<(Expression, Expression)>),
Mul(Box<(Expression, Expression)>),
Sin(Box<Expression>),
Cos(Box<Expression>),
}
impl Expression {
fn derivative(&self, var: usize) -> Expression {
use Expression::*;
match self {
Num(_) => Num(0),
Var(v) => if *v == var { Num(1) } else { Num(0) },
Add(lhs, rhs) => Add(Box::new((lhs.derivative(var), rhs.derivative(var)))),
Mul(lhs, rhs) => Add(
Box::new((Mul(Box::new(lhs.derivative(var)), rhs.clone()),
Mul(lhs.clone(), Box::new(rhs.derivative(var)))),
),
),
Sin(arg) => {
Cos(arg.clone()) * Box::new(arg.derivative(var))
}
Cos(arg) => {
Mul(
Box::new(Num(-1)),
Box::new(Mul(
Box::new(Sin(arg.clone())),
Box::new(arg.derivative(var))
))
)
}
}
}
}Doing the reverse, going from the derivative back to the original function, is much more difficult. The chain rule does not apply in reverse, and there is no general formula for anti-derivatives. For example, the derivative of exp(-x^2) is -2*x*exp(-x^2), but there is no elementary function that has exp(-x^2) as its derivative (the error function has to be introduced).
As you well know from calculus, the integral of f'(x) with respect to x is f(x) + C, where C is the constant of integration. There are many ways to present an anti-derivative, some are shorter or more useful than others.
For example, depending on the software:
\[ \int \frac{1}{e^x + 1} dx \] gives the completely differently looking solutions \(x - \log(e^x + 1)\) or \(-2 \text{arctanh}(1 + 2e^x)\) as the anti-derivative. These two solutions differ by a constant (\(i \pi\)), but that is not clear at all because different special functions are used.
There are symbolic methods to integrate. For example, Trager’s algorithm can be used to integrate any rational functions, and it has been part of Symbolica for a while. The downside is that the output of the algorithm is always in terms of logarithms, which is not always the most useful form for the end-user. For example, the integral of \(\frac{1}{x^2 + 1}\) is \(\arctan(x)\), but Trager’s algorithm will return \(\frac{i}{2}\log(1 - i x) - \frac{i}{2}\log(1 + i x)\). For a Computer Algebra System, special functions are unfortunate, as they offer more ways to write 0 in a complicated way (e.g., \(\cos(x)^2 + \sin(x)^2 -1\)), which can lead to accidental divisions by 0. In fact, detecting if two expressions are equal is undecidable in general!
There is the famous Risch decision process, which can be used to integrate elementary functions. It is a complete algorithm, but it is very complicated and there exists no full implementations of it. The Risch algorithm is also not complete for special functions, which means that there are integrals that cannot be expressed in terms of elementary functions. See the Math for Machines lecture slides for more information on this topic.
Therefore, we return to the idea of pattern matching. We can implement a set of rules that can be applied to an expression to find an anti-derivative. This is not a complete algorithm, but it works for many expressions that are encountered in practice.
In 2008, Albert Rich started work on Rubi (Rule-based Integrator), a system for symbolic integration based on pattern matching. Over the years it has grown to a set of over 7000 rules that can integrate a very broad class of expressions. The rules themselves are written in Mathematica, but the system is open-source and can be used in other languages.
Using AI assistance, we have translated the rules to Symbolica’s pattern matching language, and they are now available in Symbolica 2.2. The rules are implemented in a separate crate symbolica-integrate, which can be used in Python and Rust.
Written in Symbolica’s a matching language, where x_ is a wildcard that matches any expression, the derivative of a function can be expressed as:
derivative(cos, 1) = -sin(x)*derivative(x)
derivative(f_(g_), x_) = derivative(f_, g_)*derivative(g_, x_)
Explainable
Pattern matching upgrades
Symbolica is free for hobbyists and a single-core instance is free for non-commercial use. If you want new features, more documentation, or support for your workflow, your organization can purchase a license and directly support the project.
Closing
Symbolica 2.2 makes the symbolic system more programmable: users can teach symbols how to normalize, print, differentiate, expand around singularities, and evaluate numerically.
Let me know what you think in the comments!