Manipulating Symbolic Expressions
In engineering symbolic analysis, the need to manipulate, often algebraically, mathematical expressions arises constantly. SymPy has several powerful tools for manipulating symbolic expressions, the most useful of which we will consider here.
The simplify()
Function and Method
A built-in Sympy function and method, sp.simplify()
, is a common SymPy tool
for manipulation because simplification is often what we want. Recall
that some basic simplification occurs automatically; however, in many
cases this automatic simplification is insufficient. Applying sp.simplify()
typically results in an
expression as simple as or simpler than its input; however, the precise
meaning of “simpler” is quite vague, which can lead to frustrating cases
in which a version of an expression we consider to be simpler is not
chosen by the sp.simplify()
algorithm. In such cases, we will often use the more manual techniques
considered later in this section.
The predicates (i.e., assumptions) used to define the symbolic
variables and functions that appear in a symbolic expression are
respected by sp.simplify()
.
Consider the following example:
= sp.symbols("x", real=True)
x = (x**2 + 2*x + 3*x)/(x**2 + 2*x); e0 # For display
e0 # Returns simplified expression, leaves e0 unchanged e0.simplify()
Note that e0
was slightly
simplified automatically. The simplify()
method further simplified by
canceling an x
. The use of the
method does not affect the object, so it the same as the use of the
function.
There are a few “knobs” to turn in the form of optional arguments to
sp.simplify()
:
measure
(default:sp.count_ops()
): A function that serves as a heuristic complexity metric. The defaultsp.count_ops()
counts the operations in the expression.ratio
(default:1.7
): The maximum ratio of the measures, outputout
over inputinp
,measure(out)/measure(inp)
. Anything over1
allows the output to be potentially more complex than the input, but it may still be simpler because the metric is just a heuristic.rational
(default:False
): By default (False
), floating-point numbers are left alone. Ifrational=True
, floating-point numbers are recast as rational numbers. Ifrational=None
, floating-point numbers are recast as rational numbers during simplification, but recast to floating-point numbers in the result.inverse
(default:False
): IfTrue
, allows inverse functions to be cancelled in any order without knowing if the inner argument falls in the domain for which the inverse holds.1 For instance, this allows without knowing if .force
(default:False
): IfTrue
, predicates (assumptions) of the variables will be ignored.
Polynomial and Rational Expression Manipulation
Here we consider a few SymPy functions and methods that manipulate polynomials and rational expressions.
The expand()
Function and Method
The expand()
function and
method expresses a polynomial in the canonical form of a sum of
monomials. A monomial is a polynomial with exactly one additive term.
For instance,
+ 3)**2) ## Using the real x from above sp.expand((x
We can also expand a numerator or denominator without expanding the
entire expression, as follows for
= (x + 3)**2/(x - 2)**2
frac
frac.expand()=True)
frac.expand(numer=True)
frac.expand(denom=True).expand(denom=True) frac.expand(numer
There are several additional options for expand()
, including:
mul
(default:True
): IfTrue
, distributes multiplication over addition (e.g., .multinomial
(default:True
): IfTrue
, expands multinomial (polynomial that is not a monomial) terms into sums of monomials (e.g., ).power_exp
(default:True
): IfTrue
, expands sums in exponents to products of exponentials (e.g., ).log
(default:True
): IfTrue
, split log products into sums and extract log exponents to multiplicative constants (e.g., for , ).deep
(default:True
): IfTrue
, expands all levels of the expression tree; ifFalse
, expands only the top level (e.g., ).complex
(default:False
): IfTrue
, collect real and imaginary parts (e.g., ).func
(default:False
): IfTrue
, expand nonpolynomial functions (e.g., for the gamma function , ).trig
(default:False
): IfTrue
, expand trigonometric functions (e.g., ).
The factor()
Function and Method
The factor()
function and
method returns a factorization into irreducibles factors. For
polynomials, this is the reverse of expand()
. Irreducibility of the factors
is guaranteed for polynomials. Consider the following polynomial
example:
= sp.symbols("x, y", real=True)
x, y = (x + 1)**2 * (x**2 + 2*x*y + y**2); e0
e0
e0.expand() e0.expand().factor()
Factorization can also be performed over nonpolynomial expressions, as in the following example:
= sp.sin(x) * (sp.cos(x) + sp.sin(x))**2; e1 # Using above real x
e1
e1.expand() e1.expand().factor()
There are two options of note:
deep
(default:False
): IfTrue
, inner expression tree elements will also be factored (e.g., ).fraction
(default:True
): IfTrue
, rational expressions will be combined.
An example of the latter option is given here:
= x - 5*sp.exp(3 - x); e2 # Using real x from above
e2 =True)
e2.factor(deep=True, fraction=False) e2.factor(deep
The collect()
Function and Method
The collect()
function and
method returns an expression with specific terms collected. For
instance,
= sp.symbols("x, y, a, b", real=True)
x, y, a, b = a * x + b * x * y + a**2 * x**2 + 3 * y**2 + x * y + 8; e3
e3 e3.collect(x)
More complicated expressions can be collected as well, as in the following example:
= a*sp.cos(4*x) + b*sp.cos(4*x) + b*sp.cos(6*x) + a * sp.sin(x); e4
e4 4*x)) e4.collect(sp.cos(
Derivatives of an undefined symbolic function, as would appear in a
differential equation, can be collected. If the function is passed to
collect()
, as in the following
example, it and its derivatives are collected:
= sp.Function("f")(x) ## Applied undefined function
f = a*f.diff(x, 2) + a**2*f.diff(x) + b**2*f.diff(x) + a**3*f; e5
e5 e5.collect(f)
The rcollect()
function (not
available as a method) recursively applies collect()
. For instance,
= (a * x**2 + b*x*y + a*b*x)/(a*x**2 + b*x**2); e6
e6 # Collects in numerator and denominator sp.rcollect(e6, x)
Before collection, an expression may need to be expanded via expand()
.
The cancel()
Function and Method
The cancel()
function and
method will return an expression in the form
= (x**3 - a**3)/(x**2 - a**2); e7
e7 e7.cancel()
Note that there is an implicit assumption here that
The apart()
and together()
Functions and Methods
The apart()
function and
method returns a partial fraction expansion
of a rational expression. A partial fraction expansion rewrites a ratio
as a sum of a polynomial and one or more ratios with irreducible
denominators. It is of particular use for computing the inverse Laplace
transform. The together()
function is the complement of apart()
. Here is an example of a
partial fraction expansion:
= sp.symbols("s")
s = (s**3 + 6*s**2 + 16*s + 16)/(s**3 + 4*s**2 + 10*s + 7); e8
e8 # Partial fraction expansion
e8.apart() # Putting it back together e8.apart().together().cancel()
Trigonometric Expression Manipulation
As we saw in subsection 4.2.2, expressions including trigonometric terms can be manipulated with the SymPy functions and methods that are nominally for polynomial and rational expressions. In addition to these, considered here are two important SymPy functions and methods for manipulating expressions including trigonometric terms, with a focus on the trigonometric terms themselves.
The trigsimp()
Function and Method
The trigsimp()
function and
method attempts to simplify a symbolic expression via trigonometric
identities. For instance, it will apply the double-angle formulas, as
follows:
= sp.symbols("x", real=True)
x = 2 * sp.sin(x) * sp.cos(x); e9
e9 e9.trigsimp()
Here is a more involved expression:
= sp.cos(x)**4 - 2*sp.sin(x)**2*sp.cos(x)**2 + sp.sin(x)**4; e10
e10 e10.trigsimp()
The hyperbolic trignometric functions are also handled by trigsimp()
, as in the following
example:
= sp.cosh(x) * sp.tanh(x); e11
e11 e11.trigsimp()
The
expand_trig()
Function
The sp.expand_trig()
function
applies the double-angle or sum identity in the expansive direction,
opposite the direction of trig_simp()
; that is,
= sp.cos(x + y); e12
e12 sp.expand_trig(e12)
Power Expression Manipulation
There are three important power identities:
The powsimp()
Function and Method
The powsimp()
function and
method applies the identities of eqns. ¿eq:pow1?,
¿eq:pow2? from left-to-right (replacing the left
pattern with the right). It will only apply the identity if it holds.
Consider the following, applying eq. ¿eq:pow1?:
= sp.symbols("x", complex=True, nonzero=True)
x = sp.symbols("a, b", complex=True)
a, b = x**a * x**b; e13
e13 e13.powsimp()
Applying eq. ¿eq:pow2?,
= sp.symbols("u, v", nonnegative=True)
u, v = sp.symbols("c", real=True)
c = u**c * v**c; e14
e14 e14.powsimp()
Under certain conditions (i.e., powsimp()
appears to have no
effect. For instance,
= u**3 * v**3; e15
e15 e15.powsimp()
For expressions for which the conditions for an identity does not
hold, it can still be applied (at your own risk) via the force=True
argument.
The
expand_power_exp()
and expand_power_base()
Functions
The expand_power_exp()
function applies eq. ¿eq:pow1? from right-to-left
(opposite of powsimp()
), as
follows:
= x**(a + b); e16
e16 sp.expand_power_exp(e16)
Similarly, expand_power_base()
applies eq. ¿eq:pow2? from right-to-left (opposite of
powsimp()
, as follows:
= (u * v)**c; e17
e17 sp.expand_power_base(e17)
Again, the identity will not be applied if its conditions do not hold
for the expression; however, with the parameter force=True
,
it will be applied in any case.
The powdenest()
Function
The powdenest()
function
applies eq. ¿eq:pow3? from left-to-right. For
instance,
= sp.symbols("z, d", complex=True)
z, d = sp.symbols("n", integer=True)
n = (z**d)**n; e18
e18 sp.powdenest(e18)
However, as we see from e18
,
the denesting is automatically applied. There may be situations in which
powdenest()
must still be applied
manually.
Exponential and Logarithmic Expression Manipulation
For expand_log()
and logcombine()
functions.
The
expand_log()
Function
The expand_log()
function
applies eqns. ¿eq:log1?, ¿eq:log2?
from left-to-right. In the following example, it applies
eq. ¿eq:log1?:
= sp.symbols("x, y", positive=True)
x, y = sp.symbols("n", real=True)
n = sp.log(x * y); e19
e19 sp.expand_log(e19)
In the following example, it applies eq. ¿eq:log1?:
= sp.log(x**n); e20
e20 sp.expand_log(e20)
The
logcombine()
Function
The logcombine()
function
applies eqns. ¿eq:log1?, ¿eq:log2?
from right-to-left. In the following example, it applies
eq. ¿eq:log1?:
= sp.log(x) + sp.log(y); e21
e21 sp.logcombine(e21)
In the following example, it applies eq. ¿eq:log1?:
= n * sp.log(x); e22
e22 sp.logcombine(e22)
Rewriting Expressions in Terms of Other Functions
At times, there are identities that can translate an expression in
terms of one function (or set of functions) into an expression in terms
of another function (or set of functions). In SymPy, the rewrite()
method can perform this
translation. For instance, Euler’s formula,
= sp.symbols("x", complex=True)
x = sp.exp(1j * x); e23
e23 = e23.rewrite(sp.cos); e24 # Apply left-to-right
e24 # Apply right-to-left e24.rewrite(sp.exp)
Here is an example with a hyperbolic trigonometric function:
= sp.tanh(x); e25
e25 e25.rewrite(sp.exp)
Finally, consider the following example with trigonometric functions:
= sp.symbols("x, y", real=True)
x, y = sp.tan(x + y)**2; e26
e26 e26.rewrite(sp.cos)
Substituting and Replacing Expressions
One expression can be substituted for another via a few different methods, the two most useful of which are considered here.
The subs()
Method
The subs()
method returns a
copy of an expression with specific subexpressions replaced. There are
three ways to specify substitutions for an expression expr
:
expr.subs(old, new)
, in whichold
is replaced withnew
expr.subs(iterable)
, in whichiterable
(e.g., alist
) containsold
/new
pairs like[(old0, new0), (old1, new1), ...]
expr.subs(dictionary)
, in whichdictionary
containsold
/new
pairs like{old0: new0, old1: new1, ...}
Consider the following simple examples:
= sp.symbols("x, y, z")
x, y, z + y).subs(x, 5)
sp.sqrt(x + y**2 + z).subs({x: z, y: 2*z}) (x
By default, when an ordered iterable like a list
or tuple
is
provided, substitutions are performed in the order given, as in the
following example:
+ y).subs(((x, y), (y, z))) (x
We see that the second substitution simultaneous
, by default False
, can be
passed as True
so that
new subexpressions are ignored by later substitutions, as in the
following example:
+ y).subs(((x, y), (y, z)), simultaneous=True) (x
For dictionary substitutions, which are unordered, a canonical ordering based on the number of operations is used for reproducibility. We do not recommend relying on this canonical ordering, so if the order of substitutions is important, we recommend using an ordered iterable.
If the substitutions result in a numerical value, it will by default remain a symbolic expression:
+ y).subs(((x, 1), (y, 3)))) sp.srepr((x
'Integer(4)'
To get a numeric type from the result, the evalf()
method can be used:
1/y).subs(y, 3.0).evalf(n=20) # subs() first (20 decimal places)
(1/y).evalf(subs={y: 3.0}, n=20) # evalfr() subs (20 decimal places) (
Note that passing the substitutions to through evalf()
can result in a more accurate
representation, so this technique is preferred. We will later [TODO:
ref] return to more powerful techniques for numerical evaluation that
convert SymPy expressions to numerically evaluable functions.
The replace()
Method
The replace()
method is
similar to subs()
, but it has
matching capabilities. Common usage of the replace()
method uses wildcard variables of class sp.core.symbol.Wild
that match anything
in a pattern. For instance,
= sp.symbols("w", cls=sp.Wild)
w = sp.sin(x) + sp.sin(3*x)**2; expr
expr /w) expr.replace(sp.sin(w), sp.cos(w)
Note that the wildcard variable w
was able to match both x
and 3*x
,
and that the the wildcard could be used in the new expression as well.
In this example, and in general, these replacement rules are applied
without head to their validity, so they must be used with caution. For
more advanced usage, see the documentation on wildcard matching, sympycore
and the documentation for replacement, sympycore.
sympysimplification, sympyadvancedman, sympycore, sympycore, sympycore
The usual way of defining the inverse
is to retrict in to . This is because is not one-to-one (e.g., ), so its domain must be restricted for a proper inverse to exist. The conventional choice of domain restriction to is called the selection of a principal branch.↩︎
Online Resources for Section 4.2
No online resources.