In this article we will see how to work with rational expressions, exponentials, and logarithms using Python’s SymPy library.
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 trouble with the library’s concept of symbols, see the first article in the series about SymPy:
import sympy
# setup 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:
$$ f(x) = \frac{P(x)}{Q(x)} $$By default, SymPy does not combine or split rational expressions. For example:
a/b + c/d\(\displaystyle \frac{a}{b} + \frac{c}{d}\)
If we want to “combine” the fractions, which by hand we would do with the
least common multiple (LCM) procedure, we use the together method:
sympy.together(_)\(\displaystyle \frac{a d + b c}{b d}\)
Remember, the underscore (_) is just a shorthand for reusing a result from
a previous cell.
Now let’s see the opposite: a situation where we have a rational expression
and would like to write it as simpler fractions, called partial fractions.
A situation where we usually do this operation is when solving 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(_)\(\displaystyle x - 1 + \frac{6}{x + 2}\)
Exponentials and logarithms #
Logarithms and exponentials show up in the most diverse mathematical problem contexts. So it is important to know how to use them with SymPy, especially the implicit assumptions the package makes, which can give rise to results that are not always what we expect at first.
Starting with the notation, in most programming languages and their libraries,
log stands for the natural logarithm, which in written work we usually write
as ln. Python and SymPy are no different, but SymPy tries to be friendlier and
treats ln = log:
sympy.ln(x)\(\displaystyle \log{\left(x \right)}\)
Notice that the result shows log(x) even though we wrote ln(x) in the cell.
And remember that this is a “courtesy” of SymPy and not of Python itself. For
example, don’t try it with the standard math library:
import math
math.ln(10)---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/tmp/ipykernel_16553/2001684872.py in <module>
1 import math
2
----> 3 math.ln(10)
AttributeError: module 'math' has no attribute 'ln'
math.log(10) # this works, log being the representation of ln. Not to be confused with log10\(\displaystyle 2.30258509299405\)
math.log10(10)\(\displaystyle 1.0\)
In more basic math courses, 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 numbers, because of the branch point (discontinuity) that a logarithm has in the complex plane. 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)}\)
Notice that SymPy kept the multiplication and the 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)}\)
Notice that it “didn’t work”. Or rather, it worked, because we did not specify any assumptions about x, y, and n. So SymPy makes no implicit assumptions and considers that, in the worst case, they are complex and the expansion cannot be carried out.
Now, expand_log, like other SymPy methods that we will see in other articles
of this series, has a parameter called force that, when True, forces the
expansion to be carried out:
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)}\)
But in real life, good ideas don’t need to be forced. Or, bringing it to a context closer to programming, let’s remember the Zen of Python:
import thisThe 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 line: explicit is better than implicit. So, rather than forcing the expansion, it would be better to state the assumptions for each symbol explicitly and, if those assumptions are adequate, the expansion happens naturally.
The identities above are valid as long as x and y are positive and n is a real number. So let’s redefine those symbols, stating those assumptions explicitly:
x, y = sympy.symbols('x y', positive=True)
n = sympy.symbols('n', real=True)Let’s check whether 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, the second form is much better. Whenever you know the assumptions about each symbol (whether it is always positive, negative, real…) it is good practice to state them when creating the symbol.
If the opposite operation, combining, is desired, 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 Euler’s number, the base of 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!} $$In SymPy it is represented by E. Thus, 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 get a numerical approximation for E using methods 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 a 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 #
Another article about SymPy. Rational expressions, logarithms, and exponentials show up many times in many concepts, so what we saw in this article will be useful many times in the upcoming articles.
See you next time.