Basic Operations

Here we discuss some of the most basic operations needed for expression manipulation in SymPy. Some more advanced operations will be discussed later in the advanced expression manipulation section.

Julia differences

We access SymPy from Julia by loading either the SymPy or SymPyPythonCall packages. Once loaded, commands like the following one should run without complaint.

julia> @syms x, y, z(x, y, z)

Expand for Python example
    >>> from sympy import *
    >>> x, y, z = symbols("x y z")

Substitution

One of the most common things you might want to do with a mathematical expression is substitution. Substitution replaces all instances of something in an expression with something else. It is done using the subs method. For example

Julia differences

We can call subs using the Julian notation of subs(expr, ...) rather than the object methoc syntax more common in Python, expr.subs(...). Further, we can use "pairs" notation when calling subs in this manner.

julia> expr = cos(x) + 1cos(x) + 1
julia> subs(expr, x=>y)cos(y) + 1

Expand for Python example
    >>> expr = cos(x) + 1
    >>> expr.subs(x, y)
    cos(y) + 1

Substitution is usually done for one of two reasons:

  1. Evaluating an expression at a point. For example, if our expression is cos(x) + 1 and we want to evaluate it at the point x = 0, so that we get cos(0) + 1, which is 2.
Julia differences

We can also use the object-method syntax.

julia> expr.subs(x,0)2

Expand for Python example
   >>> expr.subs(x, 0)
   2

  1. Replacing a subexpression with another subexpression. There are two reasons we might want to do this. The first is if we are trying to build an expression that has some symmetry, such as x^{x^{x^x}}. To build this, we might start with x**y, and replace y with x**y. We would then get x**(x**y). If we replaced y in this new expression with x**x, we would get x**(x**(x**x)), the desired expression.
julia> expr = x^y y
x
julia> expr = subs(expr, y => x^y) ⎛ y⎞ ⎝x ⎠ x
julia> subs(expr, y => x^x) ⎛ ⎛ x⎞⎞ ⎜ ⎝x ⎠⎟ ⎝x ⎠ x

Expand for Python example
   >>> expr = x**y
   >>> expr
   x**y
   >>> expr = expr.subs(y, x**y)
   >>> expr
   x**(x**y)
   >>> expr = expr.subs(y, x**x)
   >>> expr
   x**(x**(x**x))

The second is if we want to perform a very controlled simplification, or perhaps a simplification that SymPy is otherwise unable to do. For example, say we have \sin(2x) + \cos(2x), and we want to replace \sin(2x) with 2\sin(x)\cos(x). As we will learn later, the function expand_trig does this. However, this function will also expand \cos(2x), which we may not want. While there are ways to perform such precise simplification, and we will learn some of them in the advanced expression manipulation section, an easy way is to just replace \sin(2x) with 2\sin(x)\cos(x).

Julia differences

As expand_trig is not exposed, it is called as a function from the sympy module, using the dot notation to access underlying values in the module.

julia> expr = sin(2x) + cos(2x)sin(2⋅x) + cos(2⋅x)
julia> sympy.expand_trig(expr) 2 2⋅sin(x)⋅cos(x) + 2⋅cos (x) - 1
julia> subs(expr, sin(2x) => 2*sin(x)* cos(x))2⋅sin(x)⋅cos(x) + cos(2⋅x)

Expand for Python example
   >>> expr = sin(2*x) + cos(2*x)
   >>> expand_trig(expr)
   2*sin(x)*cos(x) + 2*cos(x)**2 - 1
   >>> expr.subs(sin(2*x), 2*sin(x)*cos(x))
   2*sin(x)*cos(x) + cos(2*x)

There are two important things to note about subs. First, it returns a new expression. SymPy objects are immutable. That means that subs does not modify it in-place. For example

julia> expr = cos(x)cos(x)
julia> subs(expr, x=>0)1
julia> exprcos(x)
julia> xx

Expand for Python example
   >>> expr = cos(x)
   >>> expr.subs(x, 0)
   1
   >>> expr
   cos(x)
   >>> x
   x

Quick Tip

SymPy expressions are immutable. No function will change them in-place.

Julia differences

As with Pytbon, SymPy expressions are immutable. No function will change them in-place.

Here, we see that performing expr.subs(x, 0) leaves expr unchanged. In fact, since SymPy expressions are immutable, no function will change them in-place. All functions will return new expressions.

To perform multiple substitutions at once, pass a list of (old, new) pairs to subs.

julia> expr = x^3 + 4x*y - z 3
x  + 4⋅x⋅y - z
julia> subs(expr, x=>2, y=>4, z=>0)40

Expand for Python example
    >>> expr = x**3 + 4*x*y - z
    >>> expr.subs([(x, 2), (y, 4), (z, 0)])
    40

It is often useful to combine this with a list comprehension to do a large set of similar replacements all at once. For example, say we had x^4 - 4x^3 + 4x^2 - 2x + 3 and we wanted to replace all instances of x that have an even power with y, to get y^4 - 4x^3 + 4y^2 - 2x + 3.

Julia differences

We use pairs notation, though tuples could also be used

julia> expr = x^4 - 4x^3 + 4x^2 - 2x + 3 4      3      2
x  - 4⋅x  + 4⋅x  - 2⋅x + 3
julia> replacements = [x^i => y^i for i in 0:4 if iseven(i)]3-element Vector{Pair{SymPyCore.Sym{PythonCall.Core.Py}, SymPyCore.Sym{PythonCall.Core.Py}}}: 1 => 1 x^2 => y^2 x^4 => y^4
julia> subs(expr, replacements...) 3 4 2 - 4⋅x - 2⋅x + y + 4⋅y + 3

Expand for Python example
    >>> expr = x**4 - 4*x**3 + 4*x**2 - 2*x + 3
    >>> replacements = [(x**i, y**i) for i in range(5) if i % 2 == 0]
    >>> expr.subs(replacements)
    -4*x**3 - 2*x + y**4 + 4*y**2 + 3

Converting Strings to SymPy Expressions

The sympify function (that's sympify, not to be confused with simplify) can be used to convert strings into SymPy expressions.

For example

Julia differences

We can't use 3x (literal multiplication) as it isn't parsed correctly. We do not need to use a rational (e.g. 1//2), as that is parsed as desired.

julia> str_expr = "x^3 + 3*x - 1/2""x^3 + 3*x - 1/2"
julia> expr = sympify(str_expr) 3 1 x + 3⋅x - ─ 2
julia> subs(expr, x=>2)27/2

Expand for Python example
    >>> str_expr = "x**2 + 3*x - 1/2"
    >>> expr = sympify(str_expr)
    >>> expr
    x**2 + 3*x - 1/2
    >>> expr.subs(x, 2)
    19/2

Warning

sympify uses eval. Don't use it on unsanitized input.

evalf

To evaluate a numerical expression into a floating point number, use evalf.

Julia differences

We need to wrap 8 in Sym otherwise, sqrt will dispatch to the base function in Julia. Also, we could use N(expr) to get a Julia value, as evalf returns a symbolic value.

julia> expr = sqrt(Sym(8))2⋅√2
julia> expr.evalf()2.82842712474619

Expand for Python example
    >>> expr = sqrt(8)
    >>> expr.evalf()
    2.82842712474619

SymPy can evaluate floating point expressions to arbitrary precision. By default, 15 digits of precision are used, but you can pass any number as the argument to evalf. Let's compute the first 100 digits of \pi.

Julia differences

We use PI of Sym(pi) to express the symbolic value of $\pi$.

julia> PI.evalf(100)3.1415926535897932384626433832795028841971693993751058209749445923078164062862 ↪

↪ 08998628034825342117068

Expand for Python example
    >>> pi.evalf(100)
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068

To numerically evaluate an expression with a Symbol at a point, we might use subs followed by evalf, but it is more efficient and numerically stable to pass the substitution to evalf using the subs flag, which takes a dictionary of Symbol: point pairs.

Julia differences

A Julia Dict can be used when the underlying sympy method expects a Python dict.

julia> expr = cos(2x)cos(2⋅x)
julia> expr.evalf(subs=Dict(x=>2.4))0.0874989834394464

Expand for Python example
    >>> expr = cos(2*x)
    >>> expr.evalf(subs={x: 2.4})
    0.0874989834394464

Sometimes there are roundoff errors smaller than the desired precision that remain after an expression is evaluated. Such numbers can be removed at the user's discretion by setting the chop flag to True.

Julia differences

We don't use the reserved name one, as it is a base function name in Julia

julia> o = cos(Sym(1))^2 + sin(Sym(1))^2   2         2
cos (1) + sin (1)
julia> (o-1).evalf()-0.e-124
julia> (o - 1).evalf(chop=true)ERROR: Python: TypeError: must be real number, not BooleanTrue Python stacktrace: [1] evalf @ sympy.core.evalf ~/work/SymPyCore.jl/SymPyCore.jl/docs/.CondaPkg/env/lib/python3.12/site-packages/sympy/core/evalf.py:1528 [2] evalf_add @ sympy.core.evalf ~/work/SymPyCore.jl/SymPyCore.jl/docs/.CondaPkg/env/lib/python3.12/site-packages/sympy/core/evalf.py:601 [3] evalf @ sympy.core.evalf ~/work/SymPyCore.jl/SymPyCore.jl/docs/.CondaPkg/env/lib/python3.12/site-packages/sympy/core/evalf.py:1483 [4] evalf @ sympy.core.evalf ~/work/SymPyCore.jl/SymPyCore.jl/docs/.CondaPkg/env/lib/python3.12/site-packages/sympy/core/evalf.py:1648

Expand for Python example
    >>> one = cos(1)**2 + sin(1)**2
    >>> (one - 1).evalf()
    -0.e-124
    >>> (one - 1).evalf(chop=True)
    0

lambdify

subs and evalf are good if you want to do simple evaluation, but if you intend to evaluate an expression at many points, there are more efficient ways. For example, if you wanted to evaluate an expression at a thousand points, using SymPy would be far slower than it needs to be, especially if you only care about machine precision. Instead, you should use libraries like NumPy and SciPy.

The easiest way to convert a SymPy expression to an expression that can be numerically evaluated is to use the lambdify function. lambdify acts like a lambda function, except it converts the SymPy names to the names of the given numerical library, usually NumPy. For example

Julia differences

The lambdify function does not use sympy's lambdify and has room for improvement, as compared to that in the Symbolics suite.

julia> a = 0:90:9
julia> expr = sin(x)sin(x)
julia> fn = lambdify(expr)#151 (generic function with 1 method)
julia> fn.(a)10-element Vector{Float64}: 0.0 0.8414709848078965 0.9092974268256817 0.1411200080598672 -0.7568024953079282 -0.9589242746631385 -0.27941549819892586 0.6569865987187891 0.9893582466233818 0.4121184852417566

Expand for Python example
    >>> import numpy # doctest:+SKIP
    >>> a = numpy.arange(10) # doctest:+SKIP
    >>> expr = sin(x)
    >>> f = lambdify(x, expr, "numpy") # doctest:+SKIP
    >>> f(a) # doctest:+SKIP
    [ 0.          0.84147098  0.90929743  0.14112001 -0.7568025  -0.95892427
     -0.2794155   0.6569866   0.98935825  0.41211849]

Warning

lambdify uses eval. Don't use it on unsanitized input.

You can use other libraries than NumPy. For example, to use the standard library math module, use "math".

Julia differences

The library option is not available though some function equivalences may be needed

julia> fn(0.1)0.09983341664682815

Expand for Python example
    >>> f = lambdify(x, expr, "math")
    >>> f(0.1)
    0.0998334166468

To use lambdify with numerical libraries that it does not know about, pass a dictionary of sympy_name:numerical_function pairs. For example

Julia differences

While passing in a map of function values is supported, creating an arbitrary function is not. In Symbolics one can @register a function, this could be added.

julia> nothing

Expand for Python example
    >>> def mysin(x):
    ...     """
    ...     My sine. Note that this is only accurate for small x.
    ...     """
    ...     return x
    >>> f = lambdify(x, expr, {"sin":mysin})
    >>> f(0.1)
    0.1

TODO

Write an advanced numerics section