This page briefly describes the use of symbolic math for the Calculus II topics of MTH 232 within Julia utilizing the SymPy library of Python. Either the SymPy or SymPyPythonCall packages provide access, the difference being the package used for interop between Julia and Python.
The SymPy package is loaded when the MTH229 package is. We also add the Plots package to handle our plotting needs.1
usingMTH229# loads SymPy (v2.0 or greater)usingPlotsplotly()
There are a few basic ideas behind the SymPy and SymPyPythonCall packages
Symbolic variables and values can be easily created
Generic operations, such as math operations, have specialized methods for symbolic values and expressions that utilize the underlying Python library to manipulate or create new symbolic expressions.
Functions specific to the Python library are accessed through the sympy object, such as sympy.expand_trig.
Methods of objects in the Python library are called with the same dot call syntax, e.g. obj.subs(...) to call the subs method of the object
11.1 Symbolic variables
There are numerous ways to create symbolic objects in SymPy (symbols, sympify, …) but we use the @syms macro to create variables with a given name:
@syms x
(x,)
One or more variables can be created. In this example, the last one creates a range of values:
@syms x y zs[1:5]
(x, y, Sym{PyCall.PyObject}[zs₁, zs₂, zs₃, zs₄, zs₅])
Variables can have basic assumptions placed on them (positive, real), e.g.
@syms a::positive, b::real
(a, b)
Symbolic functions can be naturally specified
@symsu()u(x)
\(u{\left(x \right)}\)
11.2 Symbolic numbers
If x is symbolic, the expression 2x will also be symbolic
2x
\(2 x\)
Behind the scenes, before multiplying, 2 and x are promoted to a common type, which will be symbolic. So most regular Julia expressions involving one or more symbolic values will simply yield symbolic answers without modification. Integers and rational numbers promote to exact counterparts; floating point numbers convert to floating point values.
Still, exactness can be lost when the floating point values are created before becoming symbolic:
1/10* x +2/10* x -3/10* x
\(5.55111512312578 \cdot 10^{-17} x\)
The above “should” be \(0\), but it isn’t, due to the conversion to floating point prior to become symbolic. For example, the expression 1/10 * x first divides 10 into 1, producing a floating point approximation for 1/10 and this is promoted to symbolic when multiplied by x.
Using rational numbers or re-expressing for earlier promotion to symbolic will keep the exactness:
1//10* x +2//10* x -3//10*x, 1x/10+2x/10-3x/10
(0, 0)
Sometimes symbolic values must be created before passing to a function, so dispatch to the symbolic function occurs. Compare these two calls, where the Sym constructor is used to create symbolic numbers:
log(Sym(2)), log(2)
(log(2), 0.6931471805599453)
When in doubt, use Sym
But most of the time, just being mindful to avoid non-symbolic divisions and function calls with Julia numbers is enough.
11.3 Substitution
Substituting one value in for another, is done many ways, for example there is subs, replace, and xreplace. We discuss subs.
The subs function (or object method) replaces an old value for an new value after sympifying2 the arguments. The old/new pairs can be specified with a tuple (e.g. (old, new)), a dictionary, or the more Julian syntax, a pairs expression old => new, with multiple substitutions possibly combined in a tuple.
@syms a b c x yex = a*x^2+ b* x + c - ysubs(ex, x => y-1)
\(a \left(y - 1\right)^{2} + b \left(y - 1\right) + c - y\)
subs(ex, a =>2)
\(b x + c + 2 x^{2} - y\)
subs(ex, a=>3, b=>2, c=>1)
\(3 x^{2} + 2 x - y + 1\)
The latter is akin to function evaluation. As such, the call notation for symbolic expressions is defined to use subs:
ex(a=>3, b=>2, c=>1)
\(3 x^{2} + 2 x - y + 1\)
11.4 Numerical evaluation
An exact symbolic value may be the result of a computation, but it may be of interest to convert that value to a number (e.g. 1.414... instead of \(\sqrt{2}\)).
SymPy objects have an evalf method that finds a floating-point value.
u =exp(Sym(2)) -exp(-Sym(2))u
\(- \frac{1}{e^{2}} + e^{2}\)
The value of u is exact. In this next cell, we call the evalf method and compare to a computation within Julia:
u.evalf(), exp(2) -exp(-2)
(7.25372081569404, 7.253720815694038)
The default for .evalf() is 15 decimal points; similar but not the same as for Julia. The method allows a specification of the number of digits in its first argument. (The verbose=true argument illustrates some of the algorithm employed.)
The result of evalf looks numeric, but internally the values are symbolic values within SymPy.3 In SymPy.jl the N function converts the exact value to a Julia number type with an attempt to match the type in Python with a corresponding type in Julia. In this example, that is Rational.
u =Sym(1) /1000u.evalf(), N(u)
(0.00100000000000000, 1//1000)
For repeated conversions of expressions to numeric values, the lambdify method creates a Julia function from an expression, which can then be called as any other function.
SymPy can be used to solve many algebraically solvable equations. The solve and solveset functions are used.
By default, an expression is interpreted as an equation set equal to 0. However, we will always model the use of rhs ~ lhs to separate two sides of an equation.4
Simple assumptions on the variables are respected:
solve(y^2+1~0), solve(z +1~0)
(Any[], Any[])
Equations can have symbolic variables included. In which case, passing in the second argument the variable(s) to solve for is done:
@syms a b csolve(a*x^2+ b*x + c ~0, x)
\(\left[\begin{smallmatrix}\frac{- b - \sqrt{- 4 a c + b^{2}}}{2 a}\\\frac{- b + \sqrt{- 4 a c + b^{2}}}{2 a}\end{smallmatrix}\right]\)
The solve function has many different types of return values. The solveset function always returns a set. For finite sets, a JuliaSet object is returned. For infinite sets, this is not the case.
This may look the same as the last call using solve, but a finite set is unordered, so indexing doesn’t work unless the set is collected (using collect) into a vector.
For infinite sets, the answer is queried. In this command we check for inclusion:5
u =solveset(sin(x) ~0, x)piin u
true
Intersecting with a finite interval will create a finite set:6
S = sympy.Interval(-5, 5)intersect(S, u)
Set{Sym} with 3 elements:
0
-pi
pi
11.5.1 Numerically solving an equation
Not all equations can be solved symbolically. For example, finding the solution(s) to \(\cos(x) = x\) will result in an error:
try solve(cos(x) ~ x) catch err "No algorithms are implemented to solve equation -x + cos(x)"end
"No algorithms are implemented to solve equation -x + cos(x)"
The find_zero function of the Roots package (if new enough) accepts an equation for input:
find_zero(cos(x) ~ x, (0, pi/2)) # using [0,π/2] as a bracketing interval
Roots is re-exported by the MTH229 package.
11.5.2 Systems of equations
The solve function can solve systems of equations. These can be specified by wrapping one or more equations into a container (using parentheses to form a tuple is suggested) and specifying the symbols similarly:
@syms x::real y::realsolve((x^2+ y^2~1, y ~ x^2), (x,y))
SymPy provides function for specific topics of Calculus II.
11.6.1 Limits
Limits may be taken symbolically using limit. The specification \(x \rightarrow c\) can be specified with a tuple ((x,c)) or pair (x => c). Directional limits are specified with dir="-", dir="+" (the default), or dir="+-".
For a given set of values of a and b, we can see different answers:
N(L(a=>2, b=>3))
0.6954139679176876
Watch out for early conversion
The above example will fail if instead of making a and b symbolic and then substituting values of 2 and 3, the expression were substituted in at the outset:
As seen, SymPy returns an unevaluated limit expression, as no answer can be found. However, if the expression is simplified, SymPy will return the correct answer:
limit(simplify(up/vp), x => oo)
\(0\)
11.6.2 Differentiation
Derivatives are taken through diff(ex, var, ...) with variants for multiple or mixed derivatives. The basic usage is pretty straightforward. The derivative with respect to \(x\) of an expression is found as follows:
@syms x a bex =sin(a*x - b)diff(ex, x)
\(a \cos{\left(a x - b \right)}\)
Second derivatives can be more succinctly expressed by adding more variables:
diff(ex, x, x) # 2nd derivative
\(- a^{2} \sin{\left(a x - b \right)}\)
11.6.3 Integration
In Calculus II there are techniques of integration to be learned:
integration by parts
trigonometric integrals
partial fractions
The definition of integration can be extended to incorporate infinities:
improper integrals
Further, there are several formulas where an integral takes on a geometric meaning beyond the area under a curve:
Area between 2 curves: \(A = \int_a^b (f(x) - g(x)) dx\).
volume of revolution: \(V = \int_a^b \pi r(x)^2 dx\).
cylindrical shell: \(V = \int_a^b 2\pi x f(x) dx\).
In SymPy, the integrate function computes integrals symbolically by finding an anti-derivative. Bear in mind, not all integrands have an antiderivative that can be algebraically expressed! SymPy uses the tricks of integration above, and in addition implements an algorithm (in part) due to Risch.
The indefinite integral, \(\int f(x) dx\), is computed with integrate(f(x), x) where f(x) is some expression depending on x and may have symbolic parameters.
Definite integrals, \(\int_a^b f(x)dx\), are computed with integrate(f(x), (x, a, b)).
For example:
@syms x c a bintegrate(x *exp(x^2), x) # integration by parts
Following an example from AP, we look at the solid formed by rotating the region bounded by \(y = \sqrt{x+2}\) and \(y=e^x\) about the line \(y = -2\).
We first find the intersection points, solutions to \(f(x) = g(x)\). We will solve for these, and will have success, as SymPy uses some special functions
The integral requires two radii from the line \(y=-2\). We have on \((a,b)\) that \(g(x) > f(x) > 0\), so the distance of \(g(x)\) from \(y=-2\) is greater than that of \(f(x)\), hence the ordering below:
SymPy can’t see how to do this, we need to help. By squaring, we can see terms will cancel:
dSA =sqrt(cancel(dSA^2))
\(6 \pi\)
And
integrate(dSA, (x, -1, 2))
\(18 \pi\)
11.6.4 Algebraic manipulation
The integration of rational functions can always be done, as is known since the early days of calculus. The ability to compute a partial fraction decomposition of any rational function (of a single variable in this particular case), allows this integration as each possible term in this decomposition permits an antiderivative.
SymPy provides some tools to algebraically manipulate expressions, with some addressing rational expressions. Common algebraic manipulations include simplify/expand along with various specialized functions primarily involving polynomial expressions: factor, cancel, apart, and together.
Symbolic expressions are routinely simplified. However, as simplification can be expensive, only light simplification is done by default. For example, x^3 * x^2 is reduced to x^5; x/3 is reduced to (1/3) * x. Further manipulations are possible. The simplify function is an interface that iterates through dozens of specific simplification steps to completion.
In this example, the gamma function is \(\Gamma(n) = (n-1)!\) for integer \(n > 0\). We see by simplifying, the cancellation occurs:
The expand function takes an expression and basically multiplies it out. If new default cancellations occur, expand can actually result in shorter expressions:
expand((x-1)^3)
\(x^{3} - 3 x^{2} + 3 x - 1\)
ex = (x +1) * (x -2) - (x -1) * xex, expand(ex)
(-x*(x - 1) + (x - 2)*(x + 1), -2)
The factor function applied to polynomials is the opposite of expand. This function uses a multivariate factorization algorithm over the rational numbers. This point means not everything factorable is factored:
factor(x^2-4), factor(x^2-3)
((x - 2)*(x + 2), x^2 - 3)
Whereas, the latter factoring could be achieved through
The collect function of sympy collects common powers of a term in an expression. The collect function of Julia takes an iterable and returns a vector or array of values. As these are different concepts, the collect function must be called through sympy.collect.
ex = x*y + x -3+2*x^2- z*x^2+ x^3sympy.collect(ex, x)
The apart function finds a partial fraction decomposition for a rational expression. Such decompositions allow the integration of rational polynomials, as the result denominators are of a specific type (powers of linear or quadratic terms).
apart(ex)
\(x - 2 - \frac{3}{x}\)
The together function re-expresses such a decomposition as a rational expression.
11.6.5 Sequences and series
A finite sequence can be generated in a Julian manner using a comprehension. For example,
The sympy.sequence function can also be used to generate sequences, in this case possibly infinite ones. The specification of the values to iterate over follow the (var, start, end) pattern of integration. In the following oo is used for
There are some methods available for working with sequences which are not pursued here.
The Sum function of SymPy represents symbolic sums, both finite and infinite. It is called through sympy.Sum as sympy.Sum(ex, (i, a, b)) where ex is an expression of symbolic function in the variable i. The sum is from i=a to i=b where there is convention when a is not less than b and both are finite.
Consider a person on a ferris wheel with position coordinates \(x(t)\) and \(y(t)\). Both of these are cyclical, one side-by-side (e.g., x(t) = r(t)$, one up-and-down \((y(t) = R - r\cos(\omega t))\). A parametric description of the motion, just combines the two into a time-dependent vector
The function \(r\) is vector-valued, not scalar valued. Within Julia, vectors are created using [] with commas separating components. The above might be:
@syms R r t ωrho(t) = [r*sin(ω*t), R -r*cos(ω*t)]
rho (generic function with 1 method)
We can plot for specific values:
d = (R =>35, r=>30, ω =>2PI/120)rt =subs.(rho(t), d...) # dots to broadcast and "splat"plot(rt..., 0, 100) # dots to splat to two variable
11.7.1 Polar coordinates
Polar coordinates are a special case of a representation. The are an alternate to Cartesian descriptions and use \((r,\theta)\) as an alternate to \((x,y)\). The translation is straightforward: \((x,y) = (r\cdot \cos(\theta), r\cdot\cos(\theta))\).
There isn’t much special support for polar coordinates within base SymPy or Julia. The plot function has a projection=:polar argument that plots polar plots where \(r=r(\theta)\).
A differential equation is an equation involving a variable, a function, and its derivatives. In the following we define an operator, using Differential to simplify the specification of the equation \(y'(x) = y(x)\):
(The use of Differential just cleans up visually what can be achieved through diff(u(x), x) above.)
The dsolve function is then used to solve differential equations:
dsolve(ex)
\(u{\left(x \right)} = C_{1} e^{x}\)
The constants come from integration. Initial conditions reduce the number of constants. These are specified to the ics keyword argument using a dictionary:
dsolve(ex, ics=Dict(u(0) =>3))
\(u{\left(x \right)} = 3 e^{x}\)
The initial conditions can also involve constraints on derivatives, etc.
We follow with a much more involved example. The following equations model projectile motion using Newton’s laws and adding a drag proportional to velocity. This proportion is given by a constant \(\gamma\). We use u and v to model the \(x\) and \(y\) coordinates of motion over time, \(t\). Inverting \(x\) to get \(t\), allows the solution of \(y\) in terms of \(t\).
The difference can be visualized through plotting. The proper way to get the values for the constants is to specify numeric values in the initial conditions:
Other plotting packages can be used, of course, but there are a few recipes to easily plot SymPy expressions↩︎
The sympify function takes an expression and makes it symbolic, which can be used to convert numbers to symbolic numbers or strings into variables.↩︎
The alert reader may notice a different type face in the output below; one used for simpler symbolic expressions.↩︎
We can’t use =, the mathematical indication of an equations, as that is used for assignment within Julia, as it is with most programming languages.↩︎
There is a subtlety, pi is promoted to the symbolic value of PI before checking, as the floating point value associated to \(\pi\), 3.141592653589793, is not in the set.↩︎
To run this example requires a newer version of SymPy.↩︎