In this article we will see how to work with rational, exponential and logarithmic expressions with the SymPy library in Python.
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="mathjax",
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:
$$ f(x) = \frac{P(x)}{Q(x)} $$By default, SymPy does not combine or divide rational expressions. For example:
a/b + c/d
\(\displaystyle \frac{a}{b} + \frac{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)
\(\displaystyle \frac{a d + b c}{b 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)
\(\displaystyle \frac{x^{2} + x + 4}{x + 2}\)
sympy.apart( (x**2 + x + 4)/(x + 2) )
\(\displaystyle x - 1 + \frac{6}{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 by 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)
\(\displaystyle \log{\left(x \right)}\)
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
\(\displaystyle 2.30258509299405\)
math.log10(10)
\(\displaystyle 1.0\)
When studying mathematics at more basic levels, the following identities are taught for logarithms:
$$ \begin{align*} \log (xy) &= \log(x) + \log(y) \\ \log (x^n) &= n \log(x) \end{align*} $$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)
\(\displaystyle \log{\left(x y \right)}\)
sympy.log(x**n)
\(\displaystyle \log{\left(x^{n} \right)}\)
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))
\(\displaystyle \log{\left(x y \right)}\)
sympy.expand_log(sympy.log(x**n))
\(\displaystyle \log{\left(x^{n} \right)}\)
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)
\(\displaystyle \log{\left(x \right)} + \log{\left(y \right)}\)
sympy.expand_log(sympy.log(x**n), force=True)
\(\displaystyle n \log{\left(x \right)}\)
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))
\(\displaystyle \log{\left(x \right)} + \log{\left(y \right)}\)
sympy.expand_log(sympy.log(x**n))
\(\displaystyle n \log{\left(x \right)}\)
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))
\(\displaystyle \log{\left(x y \right)}\)
sympy.logcombine(n * sympy.log(x))
\(\displaystyle \log{\left(x^{n} \right)}\)
Finally, let’s talk about the Euler number, the base in the natural logarithm. This number has several definitions:
$$ e \equiv \lim_{n \to \infty} \left(1 + \frac{1}{n}\right)^n \equiv \lim_{x \to 0} \left(1 + x\right)^{\frac{1}{x}} \equiv \sum_{n=0}^{\infty} \frac{1}{n!} $$On SymPy, it is represented by E. Therefore, exp(x) is equivalent to E**x:
sympy.E
\(\displaystyle e\)
sympy.E**x
\(\displaystyle e^{x}\)
sympy.exp(x)
\(\displaystyle e^{x}\)
We can obtain a numerical approximation for E using ways already seen in
other
articles:
sympy.E.n()
\(\displaystyle 2.71828182845905\)
Let’s check with E that log really represents the natural logarithm:
sympy.log(sympy.E**2)
\(\displaystyle 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)
\(\displaystyle 2\)
Conclusion and more articles about SymPy #
More one article about SymPy. Rational expressions, logarithms and exponentials appear several times in several concepts, so what was seen here in this article will be useful several times in the next articles.
See you next time.