Introduction to Symbolic Computation with SymPy

python
sympy
mathematics
symbolic computing
Author

Lukman Aliyu Jibril

Published

August 15, 2023

Symbolic computation is a powerful approach in mathematics and computer science that deals with manipulating expressions and equations in their symbolic form. Unlike numerical computation, where values are approximated and manipulated, symbolic computation focuses on maintaining expressions with variables, allowing for precise mathematical manipulation. In this article, we will delve into symbolic differentiation using the popular SymPy library in Python.

Basic Numeric Approximation

When approximating the square root of 18, you might typically use the math module in Python:

import math

math.sqrt(18)
4.242640687119285

However, this result is an approximation. To work symbolically, we turn to SymPy:

import sympy

sqrt_expr = sympy.sqrt(18)
sqrt_expr

\(\displaystyle 3 \sqrt{2}\)

SymPy provides an exact symbolic representation of the square root of 18. You can also evaluate this expression numerically to a specified number of decimal places:

approx_val = sympy.N(sqrt_expr, 8)
approx_val

\(\displaystyle 4.2426407\)

Symbolic Manipulation

In SymPy, expressions are built using symbols. Here’s an example of creating a symbolic expression corresponding to the mathematical expression \(2x^{2}-xy\) :

x, y = sympy.symbols('x y')
expr = 2 * x**2 - x * y
expr

\(\displaystyle 2 x^{2} - x y\)

With symbolic expressions, you can perform various manipulations, such as addition, subtraction, and multiplication:

expr_manip = x * (expr + x * y + x**3)
expr_manip

\(\displaystyle x \left(x^{3} + 2 x^{2}\right)\)

Expressions can also be expanded and factored using the expand and factor functions, respectively.

Substitution and Evaluation

You can substitute specific values into expressions using the evalf method:

val = expr.evalf(subs={x: -1, y: 2})
val

\(\displaystyle 4.0\)

This allows you to evaluate expressions as functions:

f_symb = x ** 2
f_val = f_symb.evalf(subs={x: 3})
f_val 

\(\displaystyle 9.0\)

Numeric Operations on Symbolic Functions

To evaluate a symbolic function for each element of an array, you need to make it NumPy-compatible:

import numpy as np
from sympy.utilities.lambdify import lambdify

x_array = np.array([1, 2, 3])
f_symb_numpy = lambdify(x, f_symb, 'numpy')

result_array = f_symb_numpy(x_array)
result_array
array([1, 4, 9])

Symbolic Differentiation with SymPy

SymPy excels in symbolic differentiation. Finding derivatives is straightforward:

diff_result = sympy.diff(x**3, x)
diff_result

\(\displaystyle 3 x^{2}\)

SymPy handles standard functions and applies necessary rules for differentiation:

dfdx_composed = sympy.diff(sympy.exp(-2*x) + 3*sympy.sin(3*x), x)
print(dfdx_composed)
9*cos(3*x) - 2*exp(-2*x)

You can even differentiate the symbolic expression from before and make it NumPy-friendly:

dfdx_symb = sympy.diff(expr, x)
dfdx_symb_numpy = lambdify(x, dfdx_symb, 'numpy')

diff_result_array = dfdx_symb_numpy(x_array)
diff_result_array
array([4 - y, 8 - y, 12 - y], dtype=object)

Limitations of Symbolic Differentiation

Despite its advantages, symbolic differentiation has limitations. Complex expressions might lead to inefficient or unevaluable results. For example, consider differentiating \(|x|\) :

dfdx_abs = sympy.diff(abs(x), x)
print(dfdx_abs)
(re(x)*Derivative(re(x), x) + im(x)*Derivative(im(x), x))*sign(x)/x

Evaluating we get:

eval_result = dfdx_abs.evalf(subs={x: -2})
eval_result

\(\displaystyle - \left. \frac{d}{d x} \operatorname{re}{\left(x\right)} \right|_{\substack{ x=-2 }}\)

try:
    dfdx_abs_numpy = lambdify(x, dfdx_abs, 'numpy')
    dfdx_abs_numpy(np.array([1, -2, 0]))
except NameError as err:
    print(err)
name 'Derivative' is not defined

Numerical Differentiation

Numerical differentiation approximates derivatives using nearby points and is available through libraries like NumPy. This approach focuses on function evaluation rather than symbolic expressions:

delta_x = 0.01
numerical_derivative = (f(x + delta_x) - f(x)) / delta_x

Numerical Differentiation with NumPy

NumPy provides the np.gradient function for numerical differentiation:

import numpy as np

x_vals = np.linspace(0, 10, 100)
y_vals = np.sin(x_vals)

derivatives = np.gradient(y_vals, x_vals)
print(derivatives)
[ 0.99830036  0.99321184  0.97799815  0.95281439  0.91791729  0.8736626
  0.82050147  0.75897585  0.68971295  0.61341886  0.53087135  0.44291195
  0.35043734  0.25439024  0.15574979  0.05552157 -0.04527265 -0.14560535
 -0.2444537  -0.34080999 -0.43369194 -0.52215268 -0.6052904  -0.68225756
 -0.75226954 -0.81461261 -0.86865122 -0.91383447 -0.94970177 -0.97588745
 -0.99212457 -0.99824762 -0.99419416 -0.98000551 -0.95582633 -0.92190311
 -0.87858166 -0.82630363 -0.76560196 -0.69709546 -0.62148251 -0.53953394
 -0.45208516 -0.36002765 -0.2642999  -0.16587777 -0.06576463  0.03501895
  0.13544553  0.23449132  0.33114663  0.4244261   0.51337882  0.59709797
  0.67473008  0.74548374  0.80863767  0.86354805  0.9096551   0.94648879
  0.97367362  0.99093247  0.99808939  0.99507142  0.98190932  0.95873728
  0.92579151  0.88340789  0.83201848  0.77214717  0.7044043   0.62948048
  0.54813951  0.46121059  0.36957993  0.27418163  0.17598823  0.07600073
 -0.02476154 -0.12527139 -0.22450417 -0.32144828 -0.41511542 -0.50455072
 -0.58884245 -0.6671313  -0.73861917 -0.80257728 -0.85835363 -0.9053796
 -0.9431758  -0.97135691 -0.98963566 -0.9978257  -0.99584353 -0.98370937
 -0.96154691 -0.92958209 -0.88814077 -0.86509787]

Conclusion

Symbolic computation with SymPy offers a versatile way to manipulate mathematical expressions and perform differentiation symbolically. While powerful, it has limitations in handling complex expressions and might lead to inefficient computations. Numerical differentiation, on the other hand, provides an alternative for cases where symbolic computation might fall short. By combining these techniques, you can effectively explore and analyze mathematical functions in Python.