Rational, logarithm and exponential expressions with SymPy – Math with Python

In this article we will see how to work with rational, exponential and logarithmic expressions with the SymPy library in Python.

If you prefer to watch in video, click on the player below. The complete article is found after the video.

Importing SymPy and creating some symbols

Let’s start by importing SymPy and creating some symbols that we will use throughout the article. If you have any difficulty with the concept of symbols in the library, see the first article in the series about SymPy:

import sympy

# settings for better outputs in the article, can be ignored
sympy.init_printing(
    use_latex="png",
    scale=1.0,
    order="grlex",
    forecolor="Black",
    backcolor="White",
)

x, y, a, b, c, d, n = sympy.symbols('x y a b c d n')

Rational expressions

A rational function is any function that can be expressed as a ratio (quotient) of polynomials:

By default, SymPy does not combine or divide rational expressions. For example:

a/b + c/d

If we want to “join” the fractions, which we would manually do with the least common multiple (LCM) procedure, we use the together method:

sympy.together(a/b + c/d)

Now let’s see the opposite, a situation where we have a rational expression and we would like to write it in the form of simpler fractions, called partial fractions. A situation where we usually do this operation is in solving problems of integrals of rational functions. In this case, we use the apart method:

(x**2 + x + 4)/(x + 2)
sympy.apart( (x**2 + x + 4)/(x + 2) )

Exponential and logarithmic expressions

Logarithms and exponentials appear in the most different contexts of mathematical problems. Thus, it is important to know how to use them with SymPy, especially the implicit considerations that the package makes and that can give rise to results that are not always expected at first.

Let’s begin with notation. In most programming languages and their libraries, the representation log expresses the natural logarithm that, in written works, we usually write as ln. In Python and in SymPy it is no different, but SymPy tries to be more friendly and considers ln = log:

sympy.ln(x)

See that in the result log(x) appears even though we wrote ln(x) in the cell. And remember that this is a “friendliness” of SymPy and not of Python itself. For example, do not try to use it with the standard library math:

import math

math.ln(10)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[7], line 3
      1 import math
----> 3 math.ln(10)

AttributeError: module 'math' has no attribute 'ln'
math.log(10)  # here it works, being log the representation of ln. Do not confuse with log10
math.log10(10)

When studying mathematics at more basic levels, the following identities are taught for logarithms:

However, such identities are not valid if x and y are arbitrary complex due to the branch point (discontinuity) existing in the complex plane for a logarithm. Let’s see how SymPy handles such expressions:

sympy.log(x * y)
sympy.log(x**n)

Note that SymPy kept the multiplication and exponentiation. Let’s try to force the expansion with the expand_log method:

sympy.expand_log(sympy.log(x * y))
sympy.expand_log(sympy.log(x**n))

Apparently, it did not work. Or rather, it worked, because we did not specify boundary conditions for x, y and n. Therefore, SymPy makes no implicit considerations and considers that, in the worst case, they are complex and the expansion cannot be performed.

Now, expand_log, like other SymPy methods that we will see in articles in this series, has a parameter called force that, when True, forces the expansion to be performed:

sympy.expand_log(sympy.log(x * y), force=True)
sympy.expand_log(sympy.log(x**n), force=True)

However, in real life, good ideas do not need to be forced. Or, bringing it to a context closer to programming, let’s remember the Zen of Python:

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Look at the second verse: explicit is better than implicit. Thus, better than forcing the expansion would be to make the boundary conditions explicit for each symbol and, if such conditions are adequate, the expansion occurs naturally.

Since the above identities are valid only if x and y are positive and n is a real number, let’s redefine such symbols by specifying such conditions:

x, y = sympy.symbols('x y', positive=True)
n = sympy.symbols('n', real=True)

Let’s check if the expansions are now possible:

sympy.expand_log(sympy.log(x * y))
sympy.expand_log(sympy.log(x**n))

In my opinion, this way is much better. Whenever you know the conditions of each symbol (if it is always positive, negative, real…) it is a good practice to make it explicit when creating the symbol.

For the opposite operation, combination, there is logcombine:

sympy.logcombine(sympy.log(x) + sympy.log(y))
sympy.logcombine(n * sympy.log(x))

Finally, let’s talk about the Euler’s number, the base in the natural logarithm. This number has several definitions:

On SymPy, it is represented by E. Therefore, exp(x) is equivalent to E**x:

sympy.E
sympy.E**x
sympy.exp(x)

We can obtain a numerical approximation for E using ways already seen in other articles:

sympy.E.n()

Let’s check with E that log really represents the natural logarithm:

sympy.log(sympy.E**2)

But, what if we want the logarithm in some other base? Just pass the base as the second argument of the log method:

sympy.log(10**2, 10)

One more article about SymPy. Rational expressions, logarithms, and exponentials appear several times in several contexts, so what was seen here in this article will be useful in the next articles.

The following are the articles already published in this series:

If you want to know when new articles are available, follow Chemistry Programming on X. The complete list of articles about SymPy can be seen in the SymPy tag. And, if you would like to see all the tutorial series posts, see the SymPy tutorial tag.

See you next time.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top