Visualizing Fourier Series

Using epicycles to visualize Fourier series

Alt Text

Its fall again and I’m TAing PHYS 116C (for the third time). The first topic of PHYS 116C is Fourier series. So, naturally, I thought I’d make a post about Fourier series. At some point, I stumbled acrossed this video by The Coding Train. In this post, I’d like to replicate what the The Coding Train did, but in more generality, i.e. for a general Fourier series. I will focus on periodic functions on the interval $(-\pi, \pi)$ for simplicity (one can easily modify what I write to accomidate more general intervals). I will also be using Julia, but the code can be easily adapted to any language (but at some put you’ll need a plotting library, so I’d say Julia, python or Mathematica is the way to go.)

Fourier Series

The concept of Fourier series is incredibly useful for many fields in science, ranging from math, to physics, to engineering. As a physicist, I use Fourier series almost every day (mostly in infinite period limit, i.e. the Fourier transform, but thats a topic for a later day.) The goal of a Fourier series is to decompose a periodic function into a countably infinite number of sines and cosines with varying frequencies. This can be done for any piecewise continuous function over the real or complex numbers. Given some piecewise continuous function, $f(t)$, over the real numbers, which is periodic over the interval $(-\pi,\pi)$, we can write down its Fourier series as: $$ \begin{align} f(t) &= \dfrac{a_{0}}{2} + \sum_{n=1}^{\infty}a_{n}\cos(nt) + \sum_{n=1}^{\infty}b_{n}\sin(nt) \end{align} $$ where $a_{0}, a_{n}$ and $b_{n}$ are the Fourier coefficients. It is straight forward to compute these coefficients using a technique known as Fourier’s trick. The idea is to realize that sine and cosines with different frequencies are orthogonal over the interval $(-\pi,\pi)$ (i.e. if you integrate the product of a sine and/or cosine of different frequencies of ($-\pi,\pi$) you get zero). For example, $$ \dfrac{1}{\pi}\int_{-\pi}^{\pi}\cos(nt)\cos(mt) = \begin{cases} 0 & n\neq m\\ 1 & n=m \end{cases} $$ Similar identities hold for other combinations of sines and cosines. If we use this orthogonality of sines and cosines, we can integrate both sides of the definition of the Fourier series to isolate the $a_{n}$’s and $b_{n}$’s. The results are: $$\begin{align} a_{n} &= \dfrac{1}{\pi}\int_{-\pi}^{\pi}f(t)\cos(nt)dt, & b_{n} &= \dfrac{1}{\pi}\int_{-\pi}^{\pi}f(t)\sin(nt)dt \end{align}$$ Given these simple formulas, we can then easily compute Fourier series of any function we might like. Before doing so, I’d like to present an alternate form of the real Fourier series consisting of just cosines (which will be useful for us later on). To get rid of the sines, one can make use of the following identity: $$\begin{align} c_{n}\cos(nt + \phi_{n}) &= a_{n}\cos(nt) + b_{n}\sin(nt)\\\ c_{n} &= \sqrt{a_{n}^2 + b_{n}^2}\\ \phi_{n} &= -\tan^{-1}(b_{n}/a_{n}) \end{align}$$ (this is easiest to prove using complex exponentials.) Then, we can write down the Fourier series as: $$\begin{align} f(t) &= \sum_{n=0}^{\infty}c_{n}\cos(nt + \phi_{n}) \end{align}$$ with $c_{0} = a_{0} / 2$ and $\phi_{0} = 0$.

Before moving onto the main topic, let’s write some code to compute Fourier series. I will be using Julia (since its the best), but the reader can easily adapt the code to their favorite programing language.

First, let’s create a Julia struct for representing the Fourier series of a function:

struct FourierSeries
    a0::Float64
    ans::Array{Float64, 1}
    bns::Array{Float64, 1}
end

Let’s also create a constructor that will take in a function and return a filled FourierSeries object. To do this, we will need to compute some integrals. We will use the QuadGK library to do this (you can install via Pkg.add("QuadGK")):

using QuadGK

"""
    FourierSeries(f; N=10)

Generate a FourierSeries object containing the the first `N` (excluding a0)
Fourier coefficients of the function `f` over the interval (-π, π).
"""
function FourierSeries(f::Function; N::Int=10)
    a0::Float64 = quadgk(t-> f(t), -π, π)[1] / π
    ans = Array{Float64, 1}(undef, N)
    bns = Array{Float64, 1}(undef, N)
    for n in 1:N
        ans[n] = quadgk(t-> f(t) * cos(n * t), -π, π)[1] / π
        bns[n] = quadgk(t-> f(t) * sin(n * t), -π, π)[1] / π
    end
    FourierSeries(f, a0, ans, bns)
end

Calling FourierSeries(f) will return a FourierSeries object with the first 10 Fourier coefficients computed. Likewise, FourierSeries(f; N=100) would compute the first 100 Fourier coefficients. We will also what to be able to evaluate the Fourier series at a given time. Thus, to avoid explicitly writing the sum everytime, let’s write a function to do this:

"""
    eval(t, fs::FourierSeries)

Evaluate the Fourier series `fs` at the time `t`.
"""
function eval(t::Float64, fs::FourierSeries)
    fs.a0 / 2 + sum(fs.ans[n] * cos(n * t) + fs.bns[n] * sin(n * t)
                    for n in 1:length(fs.ans))
end

For convinience, we also make a vectorized function:

function eval(ts::AbstractRange, fs::FourierSeries)
    [eval(t, fs) for t in ts]
end

which can take in a range of times and return an array of the Fourier series evaluated at those times. Now, we can go nuts and compute the Fourier series for any function we wish. For example, we can compute the Fourier series of the step function:

# step function: zero for t < 0 and π for t > 0.
f(t) = t < 0 ? 0.0 : π;
fs = FourierSeries(f)
# Plot it!
using Plots;
ts = LinRange(-π, π, 100);
plot(ts, eval(ts, fs), linewidth=1.5, label="Fourier", framestyle=:box)
plot!(ts, [f(t) for t in ts], linewidth=2, label="f")
ylabel!("f(t)")
xlabel!("t")

Fourier series of step function

Epicycles

If you’ve ever taken a physics course, you’ve likely encountered the simple harmonic oscillator (SHO). The motion of a SHO is described by the form $x(t) = A\cos(\omega t + \phi_{0})$, where $A$ is the amplitude of the oscillator, $\omega$ is the angular frequency and $\phi_{0}$ is the initial phase angle. You may have also be taught that you can visualize the motion of the oscillator as the projection of a point moving along a circle onto the “x” axis (if you haven’t seen this, you will soon!) We’d like to take this idea one step further. Because, a Fourier series is like a sum of an infinite number of SHO, each oscillating with a different frequency.

Let’s condier the Fourier series we defined above, which we write down again here: $$\begin{align} f(t) &= \sum_{n=0}^{\infty}c_{n}\cos(nt + \phi_{n}) \end{align}$$ The first term (the $n=0$ term) is just a constant shift. The second term is where things start to get interesting, $c_{1}\cos(t+\phi_1)$. If you remember back to the “unit-circle”, this is the horizontal component of a line from the center of a circle of radius $c_{1}$ to the edge of the circle. The angle this line makes with the horizontal axis is $t + \phi_1$. Similarly, the third term, $c_{2}\cos(2t+\phi_2)$ is again the horizontal component of a line, but of a circle of radius $c_2$ and with an angle $2t+\phi_2$. Now, if we add the first three terms: $$\begin{align} c_0 + c_1\cos(t+\phi_1) + c_2\cos(2t+\phi_2) \end{align}$$ what we get can be interpreted as follows. Consider the following figure:

Fourier series of step function

This figure contains a set of circles to guide the eye along with a set of line connecting the centers of the circles. Here we have only drawn three circles, but one can imagine continuing drawing more and more. Each line drawn in this figure has a length equal to the radius of the corresponding circle, $c_n$, and makes an angle with respect to the verticle axis of $nt + \phi_n$. Consider now, the final height of the tip of the final line. It is simply $$\begin{align} c_0 + c_1\cos(t+\phi_1) + c_2\cos(2t+\phi_2) + c_3\cos(3t+\phi_3) + \cdots \end{align}$$ which is exactly the Fourier series. What we have done is gave a geometric interpretation of the Fourier series: the Fourier series is just an inifinte set of circles arranged in a funny way. We take a circle centered at ($x=0$,$y=c_0$) of radius $c_1$, then add another circle of radius $c_2$ centered at the edge of the first at an angle $t + \phi_1$, etc.. Then, the height of the center of the final circle represents the Fourier series. Note that the angles here depend on time. Therefore, all of the circles will rotate as time evolves.

This is cute and all, but let’s see it in action:

Alt Text

Now, I’d like to explain how this was made. We will use a slightly different example just so we can see more pretty pictures. In order to make the above gif, what we need to do is first construct the FourierSeries object. Let’s do this for a function that is: $$\begin{align} f(t) &= \begin{cases} t + \pi & t < 0\ \pi & t > 0\ \end{cases} \end{align}$$ To construct this object, we use:

f(t) = t < 0 ? t + π : π
fs = FourierSeries(f)

Next, we will want to make a gif. This is done by using the @gif macro in Julia (in python you can use matplotlib and its animation module. See this for a good explaination). The @gif macro takes in a for loop as its argument. That is, we use:

nframes = 1500
@gif for i = 1:nframes
    # code to make plots here...
end every 10

This will make a gif for every 10 of the plots you constructed in the for loop (see this for more information.) Inside our for loop we’d first like to just plot the Fourier series. But we would like to evolve it through time. We can do so using:

# Plot window will always be -π → π
ts = range(-π, stop = π, length = 50);
@gif for i = 1:1500
    shift = i / 100 # time shift
    plot(
        ts, # horizontal plot windows
        [eval(t + shift, fs) for t in ts], # Fourier series values
        ylims = (-1.5, π + 1.5), # fix window height
        legend = false, # let's not go for legend
        xlims = (-π, 4π), # we make some extra room for later
        framestyle = :box, # because you've got to have a frame
        aspect_ratio = 1 # so the circles look like circles and not ellipses
    )
end every 10

This will draw the Fourier series as a “traveling wave” (think of a window of width ($-\pi$,$\pi$) moving to the right.) Now comes the hard part. We would like to draw the circles similar to how we drew them before. First, let’s make a circle maker:

"""
    circle(x, y, r)

Construct x and y positions of a cicle of radius `r` centered at (`x`, `y`).
"""
function circle(x, y, r)
    θ = LinRange(0.0, 2π, 500)
    x .+ r * sin.(θ), y .+ r * cos.(θ)
end

This function will create $x$ and $y$ arrays for a circle centered at $(x,y)$ with radius $r$. We can draw the base circle, which is centered at $(0,c_0)$ using the following:

@gif for i = 1:1500
    # other code...
    x, y = (2.5π, fs.a0 / 2)
    r = sqrt(fs.ans[1]^2 + fs.bns[1]^2)
    ϕ = -atan(fs.bns[1], fs.ans[1])
    plot!(
        circle(x, y, r),
        seriestype = [:shape,],
        linecolor = :black,
        c = :white,
        legend = false
    )
end every 10

Note that we shifted the circles center to be at $x=2.5\pi$. This is so it is out of the way of the plot of the Fourier series. Now, we would like to add on more circles. The centers of the next circle will be at $(x + c_1\sin(t + \phi_1), y + c_1\cos(t + \phi_1))$. In fact, each successive circle will be located at the previous $(x,y)$ but shifted by $(c_n\sin(nt + \phi_n), c_n\cos(nt + \phi_n))$. Let’s write an internal for loop to draw all the circles:

@gif for i = 1:1500
    # other code...
    t = ts[end] + shift # get the final time
    for i = 2:length(fs.ans)
        x += r * sin((n - 1) * t + ϕ)
        y += r * cos((n - 1) * t + ϕ)
        # compute new radius and angle
        r = sqrt(fs.ans[n]^2 + fs.bns[n]^2)
        ϕ = -atan(fs.bns[n], fs.ans[n])
        plot!(
            circle(x, y, r),
            seriestype = [:shape,],
            linecolor = :black,
            c = :white,
            legend = false,
            fillalpha = 0.0
        )
    end
end every 10

This will draw all of the circles. Lastly, we would like to draw a line connecting the center of the final circle to the final time of the Fourier series plot. This can be done using:

@gif for i = 1:1500
    # other code...
    t = ts[end] + shift # get the final time
    for i = 2:length(fs.ans)
        # code...
    end
    x += r * sin(length(fs.ans) * (ts[end] + shift) + ϕ)
    y += r * cos(length(fs.ans) * (ts[end] + shift) + ϕ)
    interps = range(ts[end], stop = x, length = 50)
    plot!(interps, [y for _ in interps], color = :purple)
end every 10

And that’s it. If we put everything together, the final code is:

@gif for i = 1:1500
    shift = i / 100 #
    plot(
        ts,
        [eval(t + shift, fs) for t in ts],
        ylims = (-1.5, π + 1.5),
        legend = false,
        xlims = (-π, 4π),
        framestyle = :box,
        aspect_ratio = 1
    )
    x = 2.5π
    y = fs.a0 / 2
    r = sqrt(fs.ans[1]^2 + fs.bns[1]^2)
    ϕ = -atan(fs.bns[1], fs.ans[1])
    plot!(
        circle(x, y, r),
        seriestype = [:shape,],
        linecolor = :black,
        c = :white,
        legend = false
    )
    t = ts[end] + shift
    for n = 2:length(fs.ans)
        x += r * sin((n - 1) * t + ϕ)
        y += r * cos((n - 1) * t + ϕ)
        r = sqrt(fs.ans[n]^2 + fs.bns[n]^2)
        ϕ = -atan(fs.bns[n], fs.ans[n])
        plot!(
            circle(x, y, r),
            seriestype = [:shape,],
            linecolor = :black,
            c = :white,
            legend = false,
            fillalpha = 0.0
        )
        scatter!([x], [y], markersize = 0.3)
    end
    x += r * sin(length(fs.ans) * (ts[end] + shift) + ϕ)
    y += r * cos(length(fs.ans) * (ts[end] + shift) + ϕ)
    scatter!([x], [y], markersize = 0.1)
    interps = range(ts[end], stop = x, length = 50)
    plot!(interps, [y for _ in interps], color = :purple)
end every 10

Note that I have added some code to plot the centers of the circles using scatter!([x], [y], markersize = 0.3). Running the code will produce the following:

Alt Text

As a final note, I’ll explain how I made the gif displaying “Physics 116C” at the top of this post. What I did was plot digitize the text for “Physics 116C” using this. Using the data that I extracted from the webplot digitizer, I constructed linear interpolating functions for the $x$ and $y$ data. Then, I constructed Fourier series for $x$ and $y$ functions and essentially repeated what I showed above.