Any periodic function can be expressed as a sum of sines and cosines. Period.

That's basically what the Fourier Series is. All thanks to Joseph Fourier's study of Heat and it's propagation over matter. Fourier realized that temperature in materials could be expressed as a combination of sine and cosine waves. 

"Heat, like gravity, penetrates every substance of the universe, its rays occupy all parts of space. The object of our work is to set forth the mathematical laws which this element obeys." ~ Joseph Fourier.


Definition

The Fourier Series allows any periodic function f(x) to be expressed as a (possibly infinite) sum of sines and cosines, as follows:

Eq 1. \[ f(x) = \frac{a_0}{2} + \sum_{n=1}^\infty \, [a_n \cos(nx) + b_n \sin(nx)] \]

For this periodic function f(x), integrable in the interval [-π, π], the numbers a_n and b_n are known as the Fourier Coefficients of f, and defined as:

Eq 2. \[ a_n = \frac{1}{\pi}\int_{-\pi}^\pi f(x) \cos(nx)\, dx, \quad n \ge 0 \]

Eq 3. \[ b_n = \frac{1}{\pi}\int_{-\pi}^\pi f(x) \sin(nx)\, dx, \quad n \ge 1 \]

Application of Fourier Series in Python

The Fourier series allows a periodic function to be expressed in terms of sines and cosines. This sum is often infinite and Fourier coefficients need to be calculated too. This process is repetitive and normally a manual calculation is not practical.

Python allows to automate these calculations and to obtain a graphic result of each approximation. These are called approximations because, in many cases, the periodic function will be equal to an infinite sum of sines and cosines. Even though an infinite calculation is not always possible, an approximation can be made with a good number of iterations (100, 500, etc).

Firstly, we'll need the NumPy, SciPy, and matplotlib libraries. It's worth highlighting that the integrate module from SciPy allows to perform numerical integrations.

import numpy as np
from scipy import integrate
import matplotlib.pyplot as plt
import scipy as sp
import matplotlib as mpl

Now we proceed to calculate the first sum of equation 2. To do this, the value of a0 is calculated from equation 1.

 a0, err0 = integrate.quad(lambda x: x,-sp.pi,sp.pi) 

Here, the quad function allows us to integrate a function between two points. In this example, the function f(x) is equal to x, that is, a straight line with period 2π:

\[ f(x) = x, \quad \mathrm{for } -\pi < x < \pi, \]

\[ f(x + 2\pi) = f(x), \quad \mathrm{for } -\infty < x < \infty. \]

The lower limit is -π and the upper one is π according to equation 2. The 'quad' function returns the result of the integral and an estimate of the absolute error of the integration; These results are stored in the variables a0 and err0 respectively. We'll then divide the integral by π, according to equation 2.

a0 = (1/sp.pi)*a0
y = a0*0.5
z = np.arange(0,5*np.pi,0.01)

To graph the approximations to the Fourier series it is necessary to tabulate the values ​​of x and f(x). These approximations have the form

Eq. 4  \[ f(x)\approx\frac{a_0}{2} + \sum_{n=1}^N \, [a_n \cos(nx) + b_n \sin(nx)], \quad N \ge 0. \]

Unlike equation 1, equation 4 is simply an approximation to the Fourier series. By varying the value of N, increasingly accurate approximations are obtained. When N tends to infinity, the Fourier series of the function f(x) is obtained.

"y" is a vector that will store all the values ​​(the ordinates) of f(x). The variable y is initialized with the value a_0/2 because, as can be seen in equation 4 (and 3) a_0/2 is added to all the values ​​of the ordinates.

On the other hand, we'll the z vector to store the values ​​of the abscissas. It will store values ​​from 0 to 5 semi-periods (5p), with steps of 0.01, as follows:

z=[0,0.01,0.02,...,5π]

The range [0,5p] will allow us to graph only 5 semi-periods of the series.

We'll the proceed to calculate equation 4 will be added to the y array. To do this, the values ​​of the Fourier constants (equations 2 and 3) are calculated. 

As seen in the equations to find the Fourier coefficients a_n and b_n, the integration function must be the function f(x) multiplied by cos(nx) and sin(nx) respectively. For the case of a_0 and b_0 the cosine and sine are 1 and 0, respectively.

"y += ai*np.cos(i*z) + bi*np.sin(i*z)" updates all the entries of vector y (which, initially, are all equal to a_0/2), with a new pair or linear combination of sines and cosines. It's all about adding adding sines and cosines.

\[ z=\left [\begin{matrix} 0 \\ 0.01 \\ 0.02 \\ \vdots \\ 5\pi \\ \end{matrix}\right ] \ \wedge \ y=\left [\begin{matrix} \frac{a_0}{2} + a_1 \cos(1\cdot0) + b_1 \sin(1\cdot0) + \cdots + a_i \cos(i\cdot0) + b_i \sin(i\cdot0) + \cdots + a_N \cos(N\cdot0) + b_N \sin(N\cdot0) \\ \frac{a_0}{2} + a_1 \cos(1\cdot0.01) + b_1 \sin(1\cdot0.01) + \cdots + a_i \cos(i\cdot0.01) + b_i \sin(i\cdot0.01) + \cdots + a_N \cos(N\cdot0.01) + b_N \sin(N\cdot0.01) \\ \frac{a_0}{2} + a_1 \cos(1\cdot0.02) + b_1 \sin(1\cdot0.02) + \cdots + a_i \cos(i\cdot0.02) + b_i \sin(i\cdot0.02) + \cdots + a_N \cos(N\cdot0.02) + b_N \sin(N\cdot0.02) \\ \vdots \\ \frac{a_0}{2} + a_1 \cos(1\cdot5\pi) + b_1 \sin(1\cdot5\pi) + \cdots + a_i \cos(i\cdot5\pi) + b_i \sin(i\cdot5\pi) + \cdots + a_N \cos(N\cdot5\pi) + b_N \sin(N\cdot5\pi) \\ \end{matrix}\right ] \]

Finally, the abscissas (vector z) and ordinates (vector y) are plotted.

import numpy as np
import scipy as sp
from scipy import integrate
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

# Define fonts to use on Matplotlib plots.
TitleFont = fm.FontProperties(fname='Quicksand-Regular.otf', size=25)

# Calculation of coefficients
a0, err0 = integrate.quad(lambda x: np.exp(x), -sp.pi, sp.pi)
a0 = (1/sp.pi)*a0
y = a0*0.5
z = np.arange(0, 5*np.pi, 0.01)

# Create figure
fig = plt.figure(figsize=(5, 7))

# Number of subplots
N = 140
iterations = [1, 2, 5, 25, 120]

# Iterate over subplots
for i in range(1, N + 1):
    ai, erri = integrate.quad(lambda x: x * sp.cos(i*x), -sp.pi, sp.pi)
    ai = (1/sp.pi)*ai
    bi, erri = integrate.quad(lambda x: x * sp.sin(i*x), -sp.pi, sp.pi)
    bi = (1/sp.pi)*bi
    y += ai*np.cos(i*z) + bi*np.sin(i*z)
    
    if i in iterations:
      num_of_graphs = len(iterations)
      index = iterations.index(i) + 1

      # Add subplot
      ax = fig.add_subplot(num_of_graphs, 1, index)
      
      # Remove x-axis and y-axis numbers
      ax.xaxis.set_visible(False)
      ax.yaxis.set_visible(False)
      
      # Remove the frame around the subplot
      ax.set_frame_on(False)

      # Set title for each subplot
      ax.set_title(f'Iteration #{i}', fontproperties=TitleFont, fontsize=15)

      # Plot data
      color = '#f04859'
      ax.plot(z, y, color)

plt.subplots_adjust(wspace=2, hspace=0.5)
plt.show()

Each time the code is executed, the N-th approximation will be graphed. To choose which approximation you want to graph (or how many iterations you want to do), you just have to choose the N value in the line "N = a".