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:

x = sp.symbols("x", real=True)
e0 = (x**2 + 2*x + 3*x)/(x**2 + 2*x); e0  # For display
e0.simplify()  # Returns simplified expression, leaves e0 unchanged

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 default sp.count_ops() counts the operations in the expression.
  • ratio (default: 1.7): The maximum ratio of the measures, output out over input inp, measure(out)/measure(inp). Anything over 1 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. If rational=True, floating-point numbers are recast as rational numbers. If rational=None, floating-point numbers are recast as rational numbers during simplification, but recast to floating-point numbers in the result.
  • inverse (default: False): If True, 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 arccos(cosx)x without knowing if x[0,π].
  • force (default: False): If True, 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,

sp.expand((x + 3)**2)  ## Using the real x from above

We can also expand a numerator or denominator without expanding the entire expression, as follows for (x+3)2/(x2)2:

frac = (x + 3)**2/(x - 2)**2
frac.expand()
frac.expand(numer=True)
frac.expand(denom=True)
frac.expand(numer=True).expand(denom=True)

There are several additional options for expand(), including:

  • mul (default: True): If True, distributes multiplication over addition (e.g., 5(x+1)5x+5.
  • multinomial (default: True): If True, expands multinomial (polynomial that is not a monomial) terms into sums of monomials (e.g., (x+y)2x2+2xy+y2).
  • power_exp (default: True): If True, expands sums in exponents to products of exponentials (e.g., e3+xe3ex).
  • log (default: True): If True, split log products into sums and extract log exponents to multiplicative constants (e.g., for x,y>0, ln(x3y)3lnx+lny).
  • deep (default: True): If True, expands all levels of the expression tree; if False, expands only the top level (e.g., x(x+(y+1)2)x2+x(y+1)2).
  • complex (default: False): If True, collect real and imaginary parts (e.g., x+y(x)+(y)+j((x)+(y))).
  • func (default: False): If True, expand nonpolynomial functions (e.g., for the gamma function Γ, Γ(x+2)x2Γ(x)+xΓ(x)).
  • trig (default: False): If True, expand trigonometric functions (e.g., sin(x+y)sinxcosysinycosx).

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:

x, y = sp.symbols("x, y", real=True)
e0 = (x + 1)**2 * (x**2 + 2*x*y + y**2); e0
e0.expand()
e0.expand().factor()

Factorization can also be performed over nonpolynomial expressions, as in the following example:

e1 = sp.sin(x) * (sp.cos(x) + sp.sin(x))**2; e1  # Using above real x
e1.expand()
e1.expand().factor()

There are two options of note:

  • deep (default: False): If True, inner expression tree elements will also be factored (e.g., exp(x2+4x+4)exp((x+2)2)).
  • fraction (default: True): If True, rational expressions will be combined.

An example of the latter option is given here:

e2 = x - 5*sp.exp(3 - x); e2  # Using real x from above
e2.factor(deep=True)
e2.factor(deep=True, fraction=False)

The collect() Function and Method

The collect() function and method returns an expression with specific terms collected. For instance,

x, y, a, b = sp.symbols("x, y, a, b", real=True)
e3 = a * x + b * x * y + a**2 * x**2 + 3 * y**2 + x * y + 8; e3
e3.collect(x)

More complicated expressions can be collected as well, as in the following example:

e4 = a*sp.cos(4*x) + b*sp.cos(4*x) + b*sp.cos(6*x) + a * sp.sin(x); e4
e4.collect(sp.cos(4*x))

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:

f = sp.Function("f")(x)  ## Applied undefined function
e5 = a*f.diff(x, 2) + a**2*f.diff(x) + b**2*f.diff(x) + a**3*f; e5
e5.collect(f)

The rcollect() function (not available as a method) recursively applies collect(). For instance,

e6 = (a * x**2 + b*x*y + a*b*x)/(a*x**2 + b*x**2); e6
sp.rcollect(e6, x)  # Collects in numerator and denominator

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 p/q, where p and q are polynomials that have been expanded and have integer leading coefficients. This is typically used to cancel terms that can be factored from the numerator and denominator of a rational expression, as in the following example:

e7 = (x**3 - a**3)/(x**2 - a**2); e7
e7.cancel()

Note that there is an implicit assumption here that xa. However, the cancelation is still valid for the limit as xa.

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:

s = sp.symbols("s")
e8 = (s**3 + 6*s**2 + 16*s + 16)/(s**3 + 4*s**2 + 10*s + 7); e8
e8.apart()  # Partial fraction expansion
e8.apart().together().cancel()  # Putting it back together

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:

x = sp.symbols("x", real=True)
e9 = 2 * sp.sin(x) * sp.cos(x); e9
e9.trigsimp()

Here is a more involved expression:

e10 = sp.cos(x)**4 - 2*sp.sin(x)**2*sp.cos(x)**2 + sp.sin(x)**4; e10
e10.trigsimp()

The hyperbolic trignometric functions are also handled by trigsimp(), as in the following example:

e11 = sp.cosh(x) * sp.tanh(x); 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,

e12 = sp.cos(x + y); e12
sp.expand_trig(e12)

Power Expression Manipulation

There are three important power identities: xaxb=xa+b for x0,a,bCucvc=(uv)c for u,v0 and cR(zd)n=zdn for z,dC and nZ. Eqns. ¿eq:pow1?, ¿eq:pow2?, ¿eq:pow3? are applied in several power expression simplification functions and methods considered here.

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?:

x = sp.symbols("x", complex=True, nonzero=True)
a, b = sp.symbols("a, b", complex=True)
e13 = x**a * x**b; e13
e13.powsimp()

Applying eq. ¿eq:pow2?,

u, v = sp.symbols("u, v", nonnegative=True)
c = sp.symbols("c", real=True)
e14 = u**c * v**c; e14
e14.powsimp()

Under certain conditions (i.e., cQ, a literal rational exponent), eq. ¿eq:pow2? is applied right-to-left automatically, so powsimp() appears to have no effect. For instance,

e15 = u**3 * v**3; 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:

e16 = x**(a + b); e16
sp.expand_power_exp(e16)

Similarly, expand_power_base() applies eq. ¿eq:pow2? from right-to-left (opposite of powsimp(), as follows:

e17 = (u * v)**c; 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,

z, d = sp.symbols("z, d", complex=True)
n = sp.symbols("n", integer=True)
e18 = (z**d)**n; 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 x,y0 and nR, the following identities hold: log(xy)=log(x)+log(y)log(xn)=nlog(x) These can be applied with the 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?:

x, y = sp.symbols("x, y", positive=True)
n = sp.symbols("n", real=True)
e19 = sp.log(x * y); e19
sp.expand_log(e19)

In the following example, it applies eq. ¿eq:log1?:

e20 = sp.log(x**n); 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?:

e21 = sp.log(x) + sp.log(y); e21
sp.logcombine(e21)

In the following example, it applies eq. ¿eq:log1?:

e22 = n * sp.log(x); 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, ejx=cosx+jsinx can be applied:

x = sp.symbols("x", complex=True)
e23 = sp.exp(1j * x); e23
e24 = e23.rewrite(sp.cos); e24  # Apply left-to-right
e24.rewrite(sp.exp)  # Apply right-to-left

Here is an example with a hyperbolic trigonometric function:

e25 = sp.tanh(x); e25
e25.rewrite(sp.exp)

Finally, consider the following example with trigonometric functions:

x, y = sp.symbols("x, y", real=True)
e26 = sp.tan(x + y)**2; 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 which old is replaced with new
  • expr.subs(iterable), in which iterable (e.g., a list) contains old/new pairs like [(old0, new0), (old1, new1), ...]
  • expr.subs(dictionary), in which dictionary contains old/new pairs like {old0: new0, old1: new1, ...}

Consider the following simple examples:

x, y, z = sp.symbols("x, y, z")
sp.sqrt(x + y).subs(x, 5)
(x + y**2 + z).subs({x: z, y: 2*z})

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:

(x + y).subs(((x, y), (y, z)))

We see that the second substitution yz is applied after the first, xy. The parameter simultaneous, by default False, can be passed as True so that new subexpressions are ignored by later substitutions, as in the following example:

(x + y).subs(((x, y), (y, z)), simultaneous=True)

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:

sp.srepr((x + y).subs(((x, 1), (y, 3))))
'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,

w = sp.symbols("w", cls=sp.Wild)
expr = sp.sin(x) + sp.sin(3*x)**2; expr
expr.replace(sp.sin(w), sp.cos(w)/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


  1. The usual way of defining the inverse y=arccosx is to retrict y in x=cosy to [0,π]. This is because cos is not one-to-one (e.g., cos0=cos2π=1), so its domain must be restricted for a proper inverse to exist. The conventional choice of domain restriction to [0,π] is called the selection of a principal branch.↩︎

Online Resources for Section 4.2

No online resources.