Emre S. Tasci emre.tasci@hacettepe.edu.tr
01/09/2021
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = [12, 6]
import scipy as sp
np.random.seed(371)
$\newcommand{\unit}[1]{\,\text{#1}}$
Nature hosts a collection of harmonious processes and we do our best to imitate it. Take the music, for example, or paintings... or the perfect ordering of crystals. At our hands, we have the almost infinite possibilities though we can't say we harness most of it. When a friend asks about our favorite number, there is a very slim chance that our answer will include a number greater than 100 or for that matter a negative or even an irritational or a complex one ("3", I hear you saying 8). We confine ourselves to within a sigma of a "normal" values -- the dominancy of Gaussian distribution in almost everything we touch is a proof of that. But this is neither a boring, nor a fruitless trait, actually there is a good side to it as well, this way we can analyze things much more easily and one way to do it is the Fourier Analysis: the decomposition of complex looking signals, functions (,information) into simple sinusoids. In this treatise we'll see how it works and is applied to.
This work is limited to discrete Fourier transformation. The good news is almost all of our data comes as discrete points, from a number of measurements (or calculations).
We will start by defining the characteristics of a sinusoids, that of a wave to put it simply.
Consider a signal such as the sound wave or the shape of a rope while playing skipping rope. We immediately match their corresponding units: for the sound wave, it is the air pressure (manifested for example as the displacement of the eardrum from its equilibrium position) at a given time; while for the rope, it is the displacement of a segment of the rope at a given position. But in essance, our function representing the signal may look like this:
N = 3000
t = np.linspace(0,12,N)
x = np.sin((2*np.pi)*t/(3))
plt.plot(t,x)
plt.show()
Looking at the formula, $\sin(2\pi \frac{t}{3})$ we observe two additional coefficients next our parameter $t$: $2\pi$ and 3. "3" is the period of our signal ($T$). It means that, it takes 3 seconds to complete 1 sine wave. We have taken our observation interval from t = 0 to t = 12 s, therefore, we observe 12 / 3 = 4 full waves.
In this example, we were 'lucky' to take our observation time to 12 s, as it was an integer multiple of our period of 3 s, so we were able to directly say "4 full waves in 12 seconds". But what about the question of how many waves had passed by the 5th second? We can resort to proportionality: if in $T$ seconds 1 wave passes, then how many waves pass in 1 second? The answer is obviously: $\tfrac{1}{T}$ waves, which is defined as the frequency ($f$): the number of waves passing per second. Its unit is $\tfrac{1}{\unit{s}} = \unit{s$^{-1}$} = \unit{Hz}$ if we are measuring the change of a signal with respect to time (like a sound wave's evolution wrt time), and $\unit{m$^{-1}$}$ if we are measuring the change wrt length (like that of a rope being oscillated).
Checking the definitions, we concur that the period and the frequency are related as: $$f = \frac{1}{T} \leftrightarrow T = \frac{1}{f}$$
In general, we can write a sine or a cosine wave in the following form: $$f(t) = A\sin\left(2\pi\frac{t}{T} + \phi \right)$$
Here $A$ is the amplitude, that enables to get values outside of $[-1,1]$ domain of the sine function and $\phi$ is the phase that shifts the signal left and right (remember that cosine is just sine shifted $\tfrac{\pi}{2}$ towards left. Keeping in mind that we can always write cosine as sine (and vice versa) by introducing a phase, we'll drop the phase and from now on will be working with cosines.
$$f(t) = A\cos\left(2\pi\frac{t}{T}\right) = A\cos\left(\frac{2\pi}{T}t\right) =A\cos\left(2\pi f t\right)$$When we defined/derived frequency, we asked about the number of waves that passed in 1 second. But we can also think of the repeating motion as a rotation around a circle: at t = 0, let's say we start moving from $(1,0)$ point on the unit circle with a constant speed, then at t = T/4 we will be passing from $(0,1)$ point, at t = T/2 by the $(-1,0)$, t = 3T/4 at $(0,-1)$ and finally at t = T back at $(1,0)$. As illustrated in this image:

Harmonic Motion (from Wikipedia)
The point and beauty is, the radius of the circle actually does not matter: instead of the unit circle, it could have been a much bigger circle but given that we complete our rotation around it in T seconds, we would still be on the "top" ($\theta=\tfrac{\pi}{2}$) at t = T/4, on the "left" ($\theta=\pi$) at t = T/2, at the "bottom" ($\theta=\tfrac{3\pi}{2}$) at t = 3T/4, and back at our starting point ($\theta=2\pi$) at t = T, so it may prove advantageous to be asking the question of "how many radians are covered in 1 second?" If it takes T seconds to complete $2\pi$, then the answer is simply $\tfrac{2\pi}{T}$ radians per second (or substituting $f$ for $T$: $2\pi f$). This quantity is defined as the angular frequency ($\omega$).
$$f(t) = A\cos\left(\frac{2\pi}{T}t\right) =A\cos\left(2\pi f t\right)=A\cos\left(\omega t\right)$$and as can be easily seend, the angle covered in $t$ seconds is just $\omega t$ (radians).
When we are making a measurement, we are never measuring the whole, continous signal - we can make as many observations as we want but it is always the points from the "invisible" signal that we are observing. The more observations we make, the more reliable our graph will be with respect to the actual signal but nevertheless, always discrete. This is even present in the graphs we plot in Python.
Consider plotting a complete sine wave using 10 points (for practicality, let's assume they are uniformly distributed):
# This is the original signal for reference, shown in light gray
ty = np.linspace(0,2*np.pi,1000)
y = np.cos(2*np.pi*ty)
plt.plot(ty,y,"-",color="lightgray",lw=1)
# This is the observed signal with N = 10 measurements
N = 10
t = np.linspace(0,2*np.pi,N)
plt.plot(t,np.cos(2*np.pi*t),"o-")
plt.title("with N = {:} samples".format(N))
plt.show()
This is how we would perceive the signal if we had taken only 10 measurements. Increasing the number of observations, of course, takes us closer to the "ideal" wave that's being broadcasted. Here is another take with $N = 50$ samples:
# This is the original signal for reference, shown in light gray
ty = np.linspace(0,2*np.pi,1000)
y = np.cos(2*np.pi*ty)
plt.plot(ty,y,"-",color="lightgray",lw=1)
# This is the observed signal with N = 10 measurements
N = 50
t = np.linspace(0,2*np.pi,N)
plt.plot(t,np.cos(2*np.pi*t),"o-")
plt.title("with N = {:} samples".format(N))
plt.show()
and here's the one with $N=500$ samples:
# This is the original signal for reference, shown in light gray
ty = np.linspace(0,2*np.pi,1000)
y = np.cos(2*np.pi*ty)
plt.plot(ty,y,"-",color="lightgray",lw=1)
# This is the observed signal with N = 10 measurements
N = 500
t = np.linspace(0,2*np.pi,N)
plt.plot(t,np.cos(2*np.pi*t),"o-")
plt.title("with N = {:} samples".format(N))
plt.show()
so, the number of data that we use to identify the characteristics of the material/signal/"thing" is crucial.
Let's redefine another signal in the shape of a cosine wave, values alternating between $\pm3.2$ and taking $T=1/3\unit{s}$ to complete 1 wave. Hence:
$$x(t) = 3.2\cos(2\pi (3) t)$$The $3$ factor in the equation above is the frequency, i.e., $f=\frac{1}{T}=\frac{1}{1/3} = 3$.
Observing this wave for 4 seconds by making a total of 250 measurements over this duration, we'll see something like this:
N = 250 # Number of samples
t = np.linspace(0,4,N)
T = 1/3 # Period
f = 1/T # frequency
A = 3.2 # Amplitude
x = A * np.cos(2 * np.pi * f * t)
# Reference
t_ref = np.linspace(0,4,1000)
x_ref = A * np.cos(2 * np.pi * f * t_ref)
plt.plot(t_ref,x_ref,color="lightgrey",lw=1)
plt.plot(t,x)
plt.show()
Let's talk about the sampling: we didn't explicitly say but it is 250 measurements uniformly distributed over the 4 seconds (as defined by the linspace function), so they will be spaced by $\tfrac{4}{250}\unit{s} = 0.016\unit{s}$ apart from each other.
The above plot is actually not a continous line but these discrete 250 points connected by lines between them:
plt.plot(t_ref,x_ref,color="lightgrey",lw=1)
plt.plot(t,x,"x-")
plt.show()
Let's zoom in, say between t = 1.75 s to t = 2.00 s
t_filter = (t>=1.75) & (t<=2.0)
plt.plot(t[t_filter], x[t_filter],"x-",\
markeredgecolor="r",markeredgewidth="3")
plt.show()
So this is what sampling is: number of points taken (observations/measurements made) in a given interval. These measurements can be done uniformly like we did above or in a random way as well.
Let's do a random sampling of 250 points:
# Generate 250 random points between 0 and 4
tr = 4*np.random.rand(250)
# Calculate our -cosine- function at these points
xr = A * np.cos(2 * np.pi * f * tr)
# Plot it using these values
plt.plot(t_ref,x_ref,color="lightgrey",lw=1)
plt.plot(tr,xr,"rx-")
plt.title("What a Mess!", y=-0.15)
plt.show()
As the tr points, i.e. the instances at when we made our measurements, are not ordered, connecting them sequentially with lines creates such a mess. We'll have more luck in just plotting the data without connecting them.
plt.plot(t_ref,x_ref,color="lightgrey",lw=1)
plt.plot(tr,xr,"rx")
plt.show()
Looks like our old cosine function. To see it's indeed that, let's sort the tr points and their corresponding values. We can manage this task two ways:
tr and then recalculating xr, or,tr and re-arranging xr accordinglyThe 1st approach is much more practical for this case (and also, we should have done it so in the first time!) as the calculation of 250 points of a very simple function would take no time.
However, in real cases, the evaluation of a function can be costly and thus, re-arranging accordingly option would be much more beneficial. For this purpose, we'll follow the 2nd path and will employ np.argsort() function which returns the ordered indices of the sorted array.
sorted_indices = np.argsort(tr)
tr_sorted = tr[sorted_indices]
xr_sorted = xr[sorted_indices]
plt.plot(t_ref,x_ref,color="lightgrey",lw=1)
plt.plot(tr_sorted, xr_sorted, "rx-")
plt.show()
At some points, discrepancies can be seen but not that bad after all.
What would we end up with if instead of 250 samples, we had made 50 observations?
# Uniform sampling version
t50_u = np.linspace(0,4,50)
x50_u = A * np.cos(2 * np.pi * f * t50_u)
plt.plot(t_ref,x_ref,color="lightgrey",lw=1)
plt.plot(t50_u, x50_u, "xr-")
plt.show()
# Randomly sampled version
t50_r = 4*np.random.rand(50)
t50_r.sort()
x50_r = A * np.cos(2 * np.pi * f * t50_r)
plt.plot(t_ref,x_ref,color="lightgrey",lw=1)
plt.plot(t50_r, x50_r, "xr-")
plt.show()
(such a disaster! 8)
so, the moral of story is: Sampling matters!
_even though at the moment random sampling does not seem to be a good idea, we'll show that it actually is (on the long run) - good enough to even beat the Nyquist rate itself! (more on that in the future, and certainly with a couple of accompanying conditions 8)_
We will return to the importance of sampling once again when we get a little bit into the Fourier transformations.
We can define the signal we used above by either using the -ideally an infinite number of- samples, or by stating its amplitude & frequency. In our case, the 1st description method involves 250 values, hence the function is represented by a 250-dimensional vector:
print("Number of values:",x.size)
print(("{:.3f}, "*3+"..., "+"{:.3f}, "*2+"{:.3f}")\
.format(*x[:3],*x[-3:]))
Number of values: 250 3.200, 3.054, 2.631, ..., 2.631, 3.054, 3.200
x[:5]
array([3.2 , 3.054413 , 2.63089924, 1.96799502, 1.12601925])
The second approach is much more data-space saving with just the following 3 information:
The two approaches -in principle- yield the same signal. For some cases the first representation takes less data-space (imagine a signal that has a value of 3.2 at t = 0 point and 0 everywhere else), and for the others (like our case) the second one.
Fourier transformation is a way to go from one representation to the other one. As our data are discrete, we will be working with the Discrete Fourier Transform, defined as:
$$\begin{align*} X_k &= \sum_{n=0}^{N-1} x_n \cdot e^{-i\frac { 2\pi}{N}kn}\\ &= \sum_{n=0}^{N-1} x_n \cdot \left[\cos\left(\frac{2 \pi}{N}kn\right) - i \cdot \sin\left(\frac{2 \pi}{N}kn\right)\right] \end{align*}$$To go from $\vec X$ to $\vec x$, we can employ its reciprocal formulation:
$$\begin{align*}x_n &= \frac{1}{N} \sum_{k=0}^{N-1} X_k \cdot e^{\,i\frac{ 2 \pi}{N}kn}\\ &= \frac{1}{N}\sum_{k=0}^{N-1} X_k \cdot \left[\cos\left(\frac{2 \pi}{N}kn\right) + i \cdot \sin\left(\frac{2 \pi}{N}kn\right)\right] \end{align*} $$Among many fantastic properties of Fourier transformations, two are of critical importance to us:
* [Side note: there is a very effective and widely used method in physics called the _Density Functional Theory_ which is used to solve the Schrödinger equation, also abbreviated as "DFT", so when you see the letters DFT do not immediately assume that it stands for the Discrete Fourier Transformation!]
Before we proceed any further, let's -for a moment- forget about the formal definition of Fourier transformation and instead, focus on writing our signal in terms of addition of functions in the form of:
$$ \sum X_k \left[\cos(2\pi f_k t) + i \sin(2\pi f_k t)\right]$$since $\{X_k\}$ coefficients are complex, let's write it as $(a_k+ib_k)$:
$$ (a_k+ib_k) \left[\cos(2\pi f_k t) + i \sin(2\pi f_k t)\right]$$distributing the coefficient inside the brackets yields:
Now comes a difficult question: to represent our signal that was $\boxed{x(t) = 3.2\cos(2\pi (3) t)}$, how should we assign values to $\{a_n\}$ and $\{b_n\}$ coefficients while keeping $n$ as small as possible (in other words, to include as less contributing functions as possible)?
( ... please wait while the reader ponders ... )
The first thing that may come to your mind could be to take $a_0 = 3.2$, $b_0 = 0$ and $f_0 = 3$, but immediately after you'll realize that it won't work because the formula forces us to take the $b_n\sin(\dots)$ component alongside as well, so we'd end up with:
$$\mathscr{F}_0 \equiv 3.2\cos(2\pi 3 t) + i3.2\sin(2\pi 3 t)$$and even worse is we have a complex result in our hands whereas our original signal was purely real. We need to get rid of the imaginary part containing the sine: if we can do it, we have already pinned down the cosine, so we'd be be done!
The easiest way to get rid of it would be, to define a second contributing function, this time with $a_1 = 3.2$, $b_1 = 0$ and $f_1 = -3$ (yes, I'm aware I'm defining a "negative"(!) frequency! - more on that one in a minute). Thus we'll have from this:
$$\mathscr{F}_1 \equiv 3.2\cos(2\pi (-3) t) + i3.2\sin(2\pi (-3) t)$$Applying the trigonometric identities $\cos(-\theta) = \cos(\theta)$ and $\sin(-\theta) = -\sin(\theta)$ we get:
$$\mathscr{F}_1 \equiv 3.2\cos(2\pi (-3) t) + i3.2\sin(2\pi (-3) t) = 3.2\cos(2\pi 3 t) - i3.2\sin(2\pi 3 t) $$Almost there! So we used to contributing factors from the Fourier formula, and if sum them up now, let's see what happens:
$$\begin{align*} \mathscr{F}_0 + \mathscr{F}_1 &= \underbrace{ 3.2\cos(2\pi 3 t) + i3.2\sin(2\pi 3 t)}_{\mathscr{F}_0}+\underbrace{3.2\cos(2\pi 3 t) - i3.2\sin(2\pi 3 t)}_{\mathscr{F}_1} \\ &=6.4\cos(2\pi 3 t)\end{align*}$$So close! We're off only by the amplitude being double the original one (due to one coming from $\mathscr{F}_0 $, the other from $\mathscr{F}_1$ , but surely this is not a problem (it is actually the reason we have the $\frac{1}{N}$ factor standing in front of the summation symbol for the inverse Fourier transformation). If we take $a_0 = a_1=\frac{3.2}{2}=1.6$, $b_0 = b_1 =0$ and $f_0 =3$, $f_1 = -3$, we regain our original signal:
$$\begin{gather*}\mathscr{F}_0 \equiv 1.6\left[\cos(2\pi 3 t) + i\sin(2\pi 3 t)\right]\\ \mathscr{F}_1 \equiv 1.6\left[\cos(2\pi (-3) t) + i\sin(2\pi (-3) t)\right]\\ \Rightarrow \mathscr{F}_0 + \mathscr{F}_1 = 3.2\cos(2\pi 3 t) = x(t)\,\checkmark \end{gather*}$$YES!!! Congratulations! You have managed to evalaute all the complex looking summations with trigonometric multiplications by hand!
HW: What if, instead of the cosine signal, we had a sine signal like $x(t) = 3.2\sin(2\pi3t)$? What would be the coefficients then? Can you derive them without evaluating the summations and all?
Let's have the computer a go at the tedious job of evaluating the summations to calculate the $\vec{X}$ from a given $\vec{x}$. In our case, we have the 250 sample points from the cosine function so let's use that (the parameters and everything will be redefined and evaluated for clarity). Also, we are going to use the exponential representation for a change (nothing different as Euler's formula ensures that they are the same)
_Disclaimer: Even though $\{X_k\}$ have their values defined without the $\frac{1}{N}$, from here on, we'll just apply the division for practicality as if we're defining and using some new $X_k' \equiv \frac{X_k}{N}$ in place so we can write:_
$$x_n = \sum_{k=0}^{N-1} X_k' \cdot e^{\,i\frac{ 2 \pi}{N}kn}$$dropping the $1/N$ factor outside of the summation symbol.
# Redefining and evaluating
N = 250 # Number of samples
t = np.linspace(0,4,N) # Sampling instances
T = 1/3 # Period
f = 1/T # Frequency
A = 3.2 # Amplitude
i2pi_N = 2j*np.pi/N
x = A * np.cos(2 * np.pi * f * t)
X = np.zeros(N,complex)
for k in range(N):
for n in range(N):
X[k] += x[n]*np.exp(-i2pi_N*k*n)
X = X/N # Remember the 1/N factor!
print(X)
[ 1.28000000e-02+0.00000000e+00j 1.28874371e-02+1.61956837e-04j 1.31571819e-02+3.30745691e-04j 1.36331721e-02+5.14202103e-04j 1.43614736e-02+7.22494990e-04j 1.54225513e-02+9.70304684e-04j 1.69570421e-02+1.28095914e-03j 1.92231639e-02+1.69533277e-03j 2.27400319e-02+2.29381000e-03j 2.87137961e-02+3.26137111e-03j 4.07128507e-02+5.14322913e-03j 7.59331666e-02+1.05636149e-02j 1.57901300e+00+2.39930956e-01j -7.65545425e-02-1.26186188e-02j -3.57959129e-02-6.36333245e-03j -2.27236473e-02-4.33476755e-03j -1.63141360e-02-3.32507934e-03j -1.25269616e-02-2.71758062e-03j -1.00375563e-02-2.30997262e-03j -8.28380299e-03-2.01631033e-03j -6.98657294e-03-1.79384704e-03j -5.99163354e-03-1.61891075e-03j -5.20687514e-03-1.47732408e-03j -4.57393211e-03-1.36007211e-03j -4.05405312e-03-1.26114469e-03j -3.62051911e-03-1.17637797e-03j -3.25432330e-03-1.10279394e-03j -2.94158599e-03-1.03820481e-03j -2.67194255e-03-9.80966100e-04j -2.43750265e-03-9.29816963e-04j -2.23215792e-03-8.83773840e-04j -2.05110918e-03-8.42057705e-04j -1.89053622e-03-8.04043129e-04j -1.74736240e-03-7.69221901e-04j -1.61908378e-03-7.37176552e-04j -1.50364318e-03-7.07560773e-04j -1.39933586e-03-6.80084721e-04j -1.30473804e-03-6.54503831e-04j -1.21865199e-03-6.30610210e-04j -1.14006355e-03-6.08225923e-04j -1.06810869e-03-5.87197722e-04j -1.00204717e-03-5.67392845e-04j -9.41241445e-04-5.48695661e-04j -8.85139786e-04-5.31004950e-04j -8.33262520e-04-5.14231696e-04j -7.85190858e-04-4.98297271e-04j -7.40557711e-04-4.83131940e-04j -6.99040095e-04-4.68673617e-04j -6.60352836e-04-4.54866827e-04j -6.24243301e-04-4.41661834e-04j -5.90486984e-04-4.29013906e-04j -5.58883781e-04-4.16882686e-04j -5.29254836e-04-4.05231663e-04j -5.01439856e-04-3.94027717e-04j -4.75294821e-04-3.83240720e-04j -4.50690018e-04-3.72843208e-04j -4.27508351e-04-3.62810079e-04j -4.05643886e-04-3.53118342e-04j -3.85000579e-04-3.43746897e-04j -3.65491184e-04-3.34676336e-04j -3.47036296e-04-3.25888773e-04j -3.29563509e-04-3.17367699e-04j -3.13006690e-04-3.09097840e-04j -2.97305328e-04-3.01065050e-04j -2.82403973e-04-2.93256197e-04j -2.68251730e-04-2.85659078e-04j -2.54801817e-04-2.78262333e-04j -2.42011173e-04-2.71055367e-04j -2.29840105e-04-2.64028293e-04j -2.18251980e-04-2.57171864e-04j -2.07212945e-04-2.50477423e-04j -1.96691678e-04-2.43936855e-04j -1.86659168e-04-2.37542544e-04j -1.77088508e-04-2.31287330e-04j -1.67954726e-04-2.25164477e-04j -1.59234611e-04-2.19167640e-04j -1.50906578e-04-2.13290833e-04j -1.42950528e-04-2.07528405e-04j -1.35347733e-04-2.01875012e-04j -1.28080725e-04-1.96325601e-04j -1.21133200e-04-1.90875381e-04j -1.14489929e-04-1.85519810e-04j -1.08136674e-04-1.80254577e-04j -1.02060118e-04-1.75075584e-04j -9.62477951e-05-1.69978933e-04j -9.06880290e-05-1.64960912e-04j -8.53698778e-05-1.60017984e-04j -8.02830822e-05-1.55146771e-04j -7.54180183e-05-1.50344050e-04j -7.07656543e-05-1.45606738e-04j -6.63175113e-05-1.40931885e-04j -6.20656263e-05-1.36316664e-04j -5.80025192e-05-1.31758366e-04j -5.41211619e-05-1.27254389e-04j -5.04149500e-05-1.22802233e-04j -4.68776766e-05-1.18399496e-04j -4.35035085e-05-1.14043862e-04j -4.02869635e-05-1.09733101e-04j -3.72228906e-05-1.05465061e-04j -3.43064509e-05-1.01237664e-04j -3.15330997e-05-9.70489019e-05j -2.88985712e-05-9.28968292e-05j -2.63988631e-05-8.87795628e-05j -2.40302229e-05-8.46952756e-05j -2.17891355e-05-8.06421938e-05j -1.96723111e-05-7.66185932e-05j -1.76766750e-05-7.26227958e-05j -1.57993570e-05-6.86531669e-05j -1.40376827e-05-6.47081119e-05j -1.23891647e-05-6.07860738e-05j -1.08514952e-05-5.68855301e-05j -9.42253873e-06-5.30049905e-05j -8.10032545e-06-4.91429942e-05j -6.88304548e-06-4.52981078e-05j -5.76904337e-06-4.14689227e-05j -4.75681314e-06-3.76540533e-05j -3.84499396e-06-3.38521342e-05j -3.03236607e-06-3.00618190e-05j -2.31784729e-06-2.62817773e-05j -1.70048992e-06-2.25106938e-05j -1.17947797e-06-1.87472655e-05j -7.54124882e-07-1.49902001e-05j -4.23871529e-07-1.12382146e-05j -1.88284698e-07-7.49003265e-06j -4.70557887e-08-3.74438356e-06j -2.71782596e-16-2.46749770e-15j -4.70557893e-08+3.74438356e-06j -1.88284698e-07+7.49003265e-06j -4.23871532e-07+1.12382146e-05j -7.54124879e-07+1.49902001e-05j -1.17947797e-06+1.87472655e-05j -1.70048991e-06+2.25106938e-05j -2.31784729e-06+2.62817773e-05j -3.03236606e-06+3.00618190e-05j -3.84499396e-06+3.38521342e-05j -4.75681315e-06+3.76540533e-05j -5.76904337e-06+4.14689227e-05j -6.88304548e-06+4.52981078e-05j -8.10032545e-06+4.91429942e-05j -9.42253872e-06+5.30049905e-05j -1.08514952e-05+5.68855301e-05j -1.23891647e-05+6.07860738e-05j -1.40376827e-05+6.47081119e-05j -1.57993570e-05+6.86531668e-05j -1.76766750e-05+7.26227958e-05j -1.96723111e-05+7.66185932e-05j -2.17891355e-05+8.06421938e-05j -2.40302229e-05+8.46952756e-05j -2.63988631e-05+8.87795628e-05j -2.88985712e-05+9.28968292e-05j -3.15330997e-05+9.70489019e-05j -3.43064509e-05+1.01237664e-04j -3.72228906e-05+1.05465061e-04j -4.02869635e-05+1.09733101e-04j -4.35035084e-05+1.14043862e-04j -4.68776766e-05+1.18399496e-04j -5.04149500e-05+1.22802233e-04j -5.41211619e-05+1.27254389e-04j -5.80025192e-05+1.31758366e-04j -6.20656263e-05+1.36316664e-04j -6.63175113e-05+1.40931885e-04j -7.07656543e-05+1.45606738e-04j -7.54180183e-05+1.50344050e-04j -8.02830822e-05+1.55146771e-04j -8.53698778e-05+1.60017984e-04j -9.06880290e-05+1.64960912e-04j -9.62477951e-05+1.69978933e-04j -1.02060118e-04+1.75075584e-04j -1.08136674e-04+1.80254577e-04j -1.14489929e-04+1.85519810e-04j -1.21133200e-04+1.90875381e-04j -1.28080725e-04+1.96325601e-04j -1.35347733e-04+2.01875012e-04j -1.42950528e-04+2.07528405e-04j -1.50906578e-04+2.13290833e-04j -1.59234611e-04+2.19167640e-04j -1.67954726e-04+2.25164477e-04j -1.77088508e-04+2.31287330e-04j -1.86659168e-04+2.37542544e-04j -1.96691678e-04+2.43936855e-04j -2.07212945e-04+2.50477423e-04j -2.18251980e-04+2.57171864e-04j -2.29840105e-04+2.64028293e-04j -2.42011173e-04+2.71055367e-04j -2.54801817e-04+2.78262333e-04j -2.68251730e-04+2.85659078e-04j -2.82403973e-04+2.93256197e-04j -2.97305328e-04+3.01065050e-04j -3.13006690e-04+3.09097840e-04j -3.29563509e-04+3.17367699e-04j -3.47036296e-04+3.25888773e-04j -3.65491184e-04+3.34676336e-04j -3.85000579e-04+3.43746897e-04j -4.05643886e-04+3.53118342e-04j -4.27508351e-04+3.62810079e-04j -4.50690018e-04+3.72843208e-04j -4.75294821e-04+3.83240720e-04j -5.01439856e-04+3.94027717e-04j -5.29254836e-04+4.05231663e-04j -5.58883781e-04+4.16882686e-04j -5.90486984e-04+4.29013906e-04j -6.24243301e-04+4.41661834e-04j -6.60352836e-04+4.54866827e-04j -6.99040095e-04+4.68673617e-04j -7.40557711e-04+4.83131940e-04j -7.85190858e-04+4.98297271e-04j -8.33262520e-04+5.14231696e-04j -8.85139786e-04+5.31004950e-04j -9.41241445e-04+5.48695661e-04j -1.00204717e-03+5.67392845e-04j -1.06810869e-03+5.87197722e-04j -1.14006355e-03+6.08225923e-04j -1.21865199e-03+6.30610210e-04j -1.30473804e-03+6.54503831e-04j -1.39933586e-03+6.80084721e-04j -1.50364318e-03+7.07560773e-04j -1.61908378e-03+7.37176552e-04j -1.74736240e-03+7.69221901e-04j -1.89053622e-03+8.04043129e-04j -2.05110918e-03+8.42057705e-04j -2.23215792e-03+8.83773840e-04j -2.43750265e-03+9.29816963e-04j -2.67194255e-03+9.80966100e-04j -2.94158599e-03+1.03820481e-03j -3.25432330e-03+1.10279394e-03j -3.62051911e-03+1.17637797e-03j -4.05405312e-03+1.26114469e-03j -4.57393211e-03+1.36007211e-03j -5.20687514e-03+1.47732408e-03j -5.99163354e-03+1.61891075e-03j -6.98657294e-03+1.79384704e-03j -8.28380299e-03+2.01631033e-03j -1.00375563e-02+2.30997262e-03j -1.25269616e-02+2.71758062e-03j -1.63141360e-02+3.32507934e-03j -2.27236473e-02+4.33476755e-03j -3.57959129e-02+6.36333245e-03j -7.65545425e-02+1.26186188e-02j 1.57901300e+00-2.39930956e-01j 7.59331666e-02-1.05636149e-02j 4.07128507e-02-5.14322913e-03j 2.87137961e-02-3.26137111e-03j 2.27400319e-02-2.29381000e-03j 1.92231639e-02-1.69533277e-03j 1.69570421e-02-1.28095914e-03j 1.54225513e-02-9.70304684e-04j 1.43614736e-02-7.22494990e-04j 1.36331721e-02-5.14202103e-04j 1.31571819e-02-3.30745691e-04j 1.28874371e-02-1.61956837e-04j]
So we did the transformation and now we have a complex array $\vec X$ of the same size as $\vec x$ on our hands. So what? What did we accomplished?
Before delving into the meaning (or usefulness) of what we did, let's plot the real and imaginary parts of the array:
plt.plot(np.real(X),"b-")
plt.plot(np.imag(X),"r-")
plt.legend(["Real Part","Imaginary Part"])
plt.show()
It seems like, major part of the components are shadowed by a couple of entries in comparison. Let's find out which entries they are:
top_10_max_indices = (np.argsort(np.abs(X))[-10:])[::-1]
print ("{:^3s} {:^16s} | {:7s}".format("i","X[i]","|X[i]|"))
for i in top_10_max_indices:
print("{:3d} {:16.3f} | {:7.3f}".format(i,X[i],np.abs(X[i])))
i X[i] | |X[i]| 12 1.579+0.240j | 1.597 238 1.579-0.240j | 1.597 237 -0.077+0.013j | 0.078 13 -0.077-0.013j | 0.078 239 0.076-0.011j | 0.077 11 0.076+0.011j | 0.077 240 0.041-0.005j | 0.041 10 0.041+0.005j | 0.041 236 -0.036+0.006j | 0.036 14 -0.036-0.006j | 0.036
As can be seen, the 12th and the 238th entries differ only by the sign of the imaginary component (i.e., they are each other's complex conjugates) and have the greatest magnitude. The 2nd highest values' (top entries' consecutive entries 13th and 237th) rapidly drop to 1/20th.
The listing looks very interesting as if there is a mirror in the center (indice = 125 : all the pair members have the same distance to this point, e.g., |12--125--238|, |13--125--237|, |11--125--239|, etc. ) that changes the sign of the imaginary part from one side to the other site of it.
As these are our $\{X_k\}$s, and the signal is the same one as the one we reproduced via "hands only" calculations above, let's think about what it means. We had -very succesfully- derived $(1.6 + 0.i)$ and $(1.6 - 0.i)$ for the $X_{12}$ and $X_{238}$ (we will later talk about it being 238 instead of -12, please be patient for now). However, here the dominant contributions seem to have the amplitudes of $1.579+i0.240$ and $1.579-i0.240$, respectively. Not exactly the same but close. As the remaining ones are much lower in amplitude, let's ignore them and try to reconstruct the signal using only the 12th and 238th entries:
nn = np.arange(N)
x_recon = X[12] * np.exp(2j*np.pi*12/N*nn)\
+ X[238] * np.exp(2j*np.pi*238/N*nn)
plt.plot(t,x,"b-")
plt.plot(t,x_recon.real,"r-")
plt.legend(["original signal","reconstructed signal"]\
,loc="center right")
plt.show()
As you can observe around the left and right boundaries, it is not an exact fit, albeit a good one. If we plot the errors:
plt.plot(t,np.abs(x_recon.real - x),"-",color="lightgray")
plt.title("The difference between the original and reconstructed signals")
plt.show()
Now, we'll deal with two issues:
# Write the maximum and average magnitudes of the imaginary parts:
print("Maximum imaginary magnitude: {:}"\
.format(np.max(np.abs(x_recon.imag))))
print(" Average imaginary value: {:}"\
.format(np.mean(x_recon.imag)))
Maximum imaginary magnitude: 1.8607337892717624e-13
Average imaginary value: -6.737777002996381e-15
nn = np.arange(N)
x_recon_all = np.zeros_like(x,complex)
for i in nn:
x_recon_all += X[i] * np.exp(2j*np.pi*i/N*nn)
plt.plot(t,x,"b-")
plt.plot(t,x_recon_all.real,"r-")
plt.legend(["original signal","reconstructed signal"]\
,loc="center right")
plt.show()
# Errors
plt.plot(t,np.abs(x_recon_all.real - x),"-",color="lightgray")
plt.title("The difference between the original and "+
"reconstructed signals (Mind the $10^{-13}$ factor!)")
plt.show()
And here's a follow-up question:
We have already talked about the importance of sampling in the beginning from the point of the quality of the observed data (discrete) wrt the real signal (continous). The more measurements we made, the more the representation was faithful to the original.
From the point of transformation, number of samples possess an additional importance: the number of $X_k$s is equal to the number of $x_k$s, but their domains are different - the more numbers we have, the more sharper the distribution of the coefficients will be (they are in fact, in a way, histograms) and also will include more contributions entering into the summation concerning the addition over $n$ for each $X_k$.
To see this, let's re-transform our signal of $x(t) = 3.2\cos(2\pi (3) t)$ but this time sample it with $N=1000$ points instead of $N=250$ points like we did last time:
# Redefining and evaluating
N_1000 = 1000 # Number of samples
t_1000 = np.linspace(0,4,N_1000)
T = 1/3 # Period
f = 1/T # Frequency
A = 3.2 # Amplitude
i2pi_N_1000 = 2j*np.pi/N_1000
x_1000 = A * np.cos(2 * np.pi * f * t_1000)
X_1000 = np.zeros(N_1000,complex)
for k in range(N_1000):
for n in range(N_1000):
X_1000[k] += x_1000[n]*np.exp(-i2pi_N_1000*k*n)
X_1000 = X_1000/N_1000 # Remember the 1/N factor!
(The above calculation may take longer than the previous calculations... but no worries, once we learn about the Fast Fourier Transfrom (FFT), they will be calculated in a jiff!)
And here are the top 10 contributions, sorted. Once again we have the 12th and this time 1000 - 12 = 988th coefficients.
top_10_max_indices_1000 = (np.argsort(np.abs(X_1000))[-10:])[::-1]
print ("{:^3s} {:^16s} | {:7s}".format("i","X_1000[i]","|X_1000[i]|"))
for i in top_10_max_indices_1000:
print("{:3d} {:16.3f} | {:7.3f}"\
.format(i,X_1000[i],np.abs(X_1000[i])))
i X_1000[i] | |X_1000[i]| 12 1.599+0.060j | 1.600 988 1.599-0.060j | 1.600 989 0.020-0.001j | 0.020 11 0.020+0.001j | 0.020 987 -0.019+0.001j | 0.019 13 -0.019-0.001j | 0.019 990 0.010-0.000j | 0.010 10 0.010+0.000j | 0.010 14 -0.009-0.000j | 0.009 986 -0.009+0.000j | 0.009
nn_1000 = np.arange(N_1000)
x_recon_1000 = X_1000[12] * np.exp(2j*np.pi*12/N_1000*nn_1000)\
+ X_1000[N_1000-12] * np.exp(2j*np.pi*(N_1000-12)/N_1000*nn_1000)
plt.plot(t,x,"b-")
plt.plot(t_1000,x_recon_1000.real,"r-")
plt.legend(["original signal","reconstructed signal"]\
,loc="center right")
plt.show()
We will return to the importance of sampling once more when we deal with a mixed signal (as opposed to a pure sinusoidal one we have been tackling with), to see that greater number of samples does also remove a faulty splitting in frequencies (but you didn't hear it from me - yet ;)
Let's reconstruct the signal and our coefficients from scratch, using $N=250$ samples as we did:
# Redefining and evaluating
N = 250 # Number of samples
t = np.linspace(0,4,N) # Sampling instances
T = 1/3 # Period
f = 1/T # Frequency
A = 3.2 # Amplitude
i2pi_N = 2j*np.pi/N
x = A * np.cos(2 * np.pi * f * t)
X = np.zeros(N,complex)
for k in range(N):
for n in range(N):
X[k] += x[n]*np.exp(-i2pi_N*k*n)
X = X/N # Remember the 1/N factor!
plt.plot(np.real(X),"b-")
plt.plot(np.imag(X),"r-")
plt.legend(["Real Part","Imaginary Part"])
plt.show()
top_10_max_indices = (np.argsort(np.abs(X))[-10:])[::-1]
print ("{:^3s} {:^16s} | {:7s}".format("i","X[i]","|X[i]|"))
for i in top_10_max_indices:
print("{:3d} {:16.3f} | {:7.3f}".format(i,X[i],np.abs(X[i])))
i X[i] | |X[i]| 12 1.579+0.240j | 1.597 238 1.579-0.240j | 1.597 237 -0.077+0.013j | 0.078 13 -0.077-0.013j | 0.078 239 0.076-0.011j | 0.077 11 0.076+0.011j | 0.077 240 0.041-0.005j | 0.041 10 0.041+0.005j | 0.041 236 -0.036+0.006j | 0.036 14 -0.036-0.006j | 0.036
Here, the real parts' magnitudes are greater than the imaginary parts because they are related to cosine affinity whereas the imaginary coefficients are related to sines.
This is due to the fact that when we add up two corresponding frequencies' contributions, as our signal is real, the imaginary parts should cancel each other and given that the coefficients $X_k$ and $X_{N-k}$ are complex conjugates of each other in the form $a+ib$ and $a-ib$, we end up with:
$$\begin{gather*} \underbrace{(a+ib)}_{X_k}[\cos\theta + i\sin\theta] + \underbrace{(a-ib)}_{X_{N-k}}[\cos(-\theta) + i\sin(-\theta)]\\ =(a+ib)[\cos\theta + i\sin\theta] + (a-ib)[\cos\theta - i\sin\theta]\\ =(a\cos\theta+ia\sin\theta+ib\cos\theta-b\sin\theta)\\+ (a\cos\theta -ia\sin\theta-ib\cos\theta-b\sin\theta)\\ =\boxed{2a\cos(\theta) - 2b\sin(\theta)} \end{gather*}$$For the pure cosine wave we have: $X_{12} = 1.579+i0.240$ and $X_{238} = 1.579-i0.240$ meaning $3.158\cos(\dots)-0.48\sin(\dots)$ Not the perfect result when compared to our actual signal of $3.2\cos(\dots) - 0.0\sin(\dots)$ but this is due to the relatively low number of samples. You can verify that with a higher number of samples, the coefficients converge to $(3.200, 0.000)$ (when we went from $N=250$ to $N=1000$ above, we had achieved $(3.198,0.120)$ already!).
And if we had a pure sine function, then the spikes would be maximized in the imaginary section. You can observe this by yourselves by running the following code (everything we've done so far, with only the "cos" replaced by "sin")
N = 250 # Number of samples
t = np.linspace(0,4,N)
T = 1/3 # Period
A = 3.2 # Amplitude
i2pi_N = 2j*np.pi/N
xs = A * np.sin(2 * np.pi / T * t)
Xs = np.zeros(N,complex)
for k in range(N):
for n in range(N):
Xs[k] += xs[n]*np.exp(-i2pi_N*k*n)
Xs /= N
plt.plot(np.real(Xs),"b-")
plt.plot(np.imag(Xs),"r-")
plt.legend(["Real Part","Imaginary Part"])
plt.show()
(the mysterious $(\theta)$ parameters will be revealed in the "One last remark" section once we get past the 'negative' frequencies)
Checking the formulation of the inverse transformation we see that $X_k$ plays the role of amplitude (& phase):
$$x_n = \frac{1}{N} \sum_{k=0}^{N-1} X_k\cdot e^{i 2 \pi k n / N}= \frac{1}{N} \sum_{k=0}^{N-1}X_k\cdot\left[\cos\left(\frac{2 \pi}{N}kn\right) + i \cdot \sin\left(\frac{2 \pi}{N}kn\right)\right]$$As $|X_{12}|$ is much greater than all the remaining, let's assume that, this (along with the 238. component) is the only contributing factor and all the remaining coefficients are zero (/ignorable), and reconstruct $\vec x$ (as xx) using only these two components:
xx = np.zeros(N,complex)
n = np.arange(N)
for k in [12,238]:
xx += X[k]*(np.cos(2*np.pi/N*k*n)+1j*np.sin(2*np.pi/N*k*n))
Our initial signal was, naturally, real. However from the formula, contribution from the imaginary part can be seen... but is it so? Let's check the constructed signal:
print(xx)
[ 3.158026 +2.71727085e-14j 2.87294812+2.54241073e-14j 2.3285266 +2.13162821e-14j 1.57390685+1.46549439e-14j 0.67720911+8.21565038e-15j -0.28062092+4.44089210e-16j -1.21311904-6.21724894e-15j -2.03610781-1.73194792e-14j -2.67529526-1.97619698e-14j -3.07298138-1.92068583e-14j -3.19326667-2.74780199e-14j -3.02529287-2.30926389e-14j -2.58422313-1.68753900e-14j -1.90987321-1.11022302e-14j -1.0631173 -1.35447209e-14j -0.12039283-3.99680289e-15j 0.83319962+3.55271368e-15j 1.71157838+7.77156117e-15j 2.4354514 +9.10382880e-15j 2.939474 +2.86437540e-14j 3.17814761+2.73392420e-14j 3.12992694+2.46469511e-14j 2.79916492+1.87627691e-14j 2.21571973+1.26565425e-14j 1.4322595 +7.32747196e-15j 0.51950791+3.77475828e-15j -0.44014014+2.44249065e-15j -1.36005634-1.55431223e-14j -2.15719901-2.39808173e-14j -2.75960937-2.68673972e-14j -3.11290727-2.80886425e-14j -3.1852002 -2.35922393e-14j -2.96996221-1.98729921e-14j -2.48662302-1.48769885e-14j -1.77881411-8.43769499e-15j -0.91043003-4.88498131e-15j 0.04013939-2.66453526e-15j 0.9870854 -3.33066907e-15j 1.84492631+1.95399252e-14j 2.53622404+2.56461519e-14j 2.99857452+2.57571742e-14j 3.19024095+2.50077736e-14j 3.09392142+2.19824159e-14j 2.71831079+1.50990331e-14j 2.09731576+1.06581410e-14j 1.28699414+5.32907052e-15j 0.36049438+3.10862447e-15j -0.59854753+3.10862447e-15j -1.50355802+5.10702591e-15j -2.27284095+8.65973959e-15j -2.83695246+1.46549439e-14j -3.14496969+1.86517468e-14j -3.16908764+2.39530618e-14j -2.90712917+2.40918396e-14j -2.38274149-4.44089210e-14j -1.64326158-2.86437540e-14j -0.75544294-1.48769885e-14j 0.20057022+2.22044605e-16j 1.13847772+1.11022302e-14j 1.9736138 +2.15383267e-14j 2.63058996+2.48689958e-14j 3.05010038+2.39253062e-14j 3.19427546+1.98452366e-14j 3.05010038+1.93178806e-14j 2.63058996+1.25455202e-14j 1.9736138 +6.66133815e-15j 1.13847772+5.10702591e-15j 0.20057022+2.88657986e-15j -0.75544294+3.99680289e-15j -1.64326158+8.21565038e-15j -2.38274149+1.04360964e-14j -2.90712917+1.73194792e-14j -3.16908764+2.40085729e-14j -3.14496969+2.33701947e-14j -2.83695246+2.56461519e-14j -2.27284095-4.08562073e-14j -1.50355802-2.53130850e-14j -0.59854753-1.15463195e-14j 0.36049438+2.22044605e-15j 1.28699414+1.19904087e-14j 2.09731576+1.75415238e-14j 2.71831079+2.33146835e-14j 3.09392142+2.19269047e-14j 3.19024095+1.74027459e-14j 2.99857452+1.67643677e-14j 2.53622404+1.03250741e-14j 1.84492631+4.88498131e-15j 0.9870854 +3.01980663e-14j 0.04013939+2.66453526e-15j -0.91043003-2.13162821e-14j -1.77881411+9.76996262e-15j -2.48662302-5.81756865e-14j -2.96996221+1.99840144e-14j -3.1852002 -6.40598685e-14j -3.11290727+3.09197112e-14j -2.75960937-5.16253706e-14j -2.15719901+2.44249065e-14j -1.36005634-2.22044605e-14j -0.44014014+3.33066907e-15j 0.51950791+3.77475828e-15j 1.4322595 -2.81996648e-14j 2.21571973+1.70974346e-14j 2.79916492-5.71764858e-14j 3.12992694+1.98729921e-14j 3.17814761-7.53563878e-14j 2.939474 +1.42108547e-14j 2.4354514 -6.12843110e-14j 1.71157838+6.66133815e-15j 0.83319962+2.55351296e-14j -0.12039283+3.10862447e-15j -1.0631173 -2.22044605e-14j -1.90987321+1.22124533e-14j -2.58422313-5.79536419e-14j -3.02529287+2.79776202e-14j -3.19326667-6.18394225e-14j -3.07298138+2.74225087e-14j -2.67529526-4.32986980e-14j -2.03610781+2.44249065e-14j -1.21311904-2.10942375e-14j -0.28062092+1.99840144e-15j 0.67720911+4.88498131e-15j 1.57390685-2.93098879e-14j 2.3285266 +1.64313008e-14j 2.87294812-6.10622664e-14j 3.158026 +1.20181642e-14j 3.158026 -7.72715225e-14j 2.87294812+1.16573418e-14j 2.3285266 -6.43929354e-14j 1.57390685+2.44249065e-15j 0.67720911+2.17603713e-14j -0.28062092+3.77475828e-15j -1.21311904-2.68673972e-14j -2.03610781+1.04360964e-14j -2.67529526-5.37347944e-14j -3.07298138+2.51465515e-14j -3.19326667-5.37347944e-14j -3.02529287+3.45279361e-14j -2.58422313-4.44089210e-14j -1.90987321+2.73114864e-14j -1.0631173 -1.62092562e-14j -0.12039283-1.11022302e-15j 0.83319962+4.88498131e-15j 1.71157838-3.57491814e-14j 2.4354514 +1.95399252e-14j 2.939474 -6.99440506e-14j 3.17814761+1.53488333e-14j 3.12992694-7.33857419e-14j 2.79916492+4.32986980e-15j 2.21571973-5.88418203e-14j 1.4322595 -1.55431223e-15j 0.51950791+1.59872116e-14j -0.44014014+3.99680289e-15j -1.36005634-2.73114864e-14j -2.15719901+1.64313008e-14j -2.75960937-5.81756865e-14j -3.11290727+3.34177130e-14j -3.1852002 -5.68850522e-14j -2.96996221+3.10862447e-14j -2.48662302-3.64153152e-14j -1.77881411+2.35367281e-14j -0.91043003-1.17683641e-14j 0.04013939-3.33066907e-15j 0.9870854 +7.10542736e-15j 1.84492631-4.32986980e-14j 2.53622404+1.39888101e-14j 2.99857452-6.81676937e-14j 3.19024095+7.35522754e-15j 3.09392142-8.02691247e-14j 2.71831079+7.10542736e-15j 2.09731576-6.08402217e-14j 1.28699414+4.44089210e-16j 0.36049438+1.24344979e-14j -0.59854753+5.99520433e-15j -1.50355802-3.19744231e-14j -2.27284095-1.06137321e-13j -2.83695246+1.08579812e-13j -3.14496969+3.04756220e-14j -3.16908764-4.85445018e-14j -2.90712917-1.27675648e-13j -2.38274149+9.79216708e-14j -1.64326158+2.57571742e-14j -0.75544294-1.13242749e-14j 0.20057022+5.10702591e-15j 1.13847772-5.92859095e-14j 1.9736138 -4.39648318e-14j 2.63058996+1.72084569e-14j 3.05010038+9.64228697e-14j 3.19427546-1.70918835e-13j 3.05010038-8.68749517e-14j 2.63058996+2.22044605e-16j 1.9736138 +5.72875081e-14j 1.13847772-6.70574707e-14j 0.20057022-3.77475828e-15j -0.75544294+6.21724894e-15j -1.64326158-3.06421555e-14j -2.38274149-1.13686838e-13j -2.90712917+1.08357767e-13j -3.16908764+3.86635168e-14j -3.14496969-5.14033260e-14j -2.83695246-1.17461596e-13j -2.27284095+9.90318938e-14j -1.50355802+2.19824159e-14j -0.59854753-7.54951657e-15j 0.36049438+1.06581410e-14j 1.28699414-6.48370246e-14j 2.09731576-5.17363929e-14j 2.71831079+1.09912079e-14j 3.09392142+1.00974784e-13j 3.19024095-1.78773663e-13j 2.99857452-8.23785484e-14j 2.53622404+2.88657986e-15j 1.84492631+4.86277685e-14j 0.9870854 -5.66213743e-14j 0.04013939+1.11022302e-15j -0.91043003+5.77315973e-15j -1.77881411-3.55271368e-14j -2.48662302-1.12576615e-13j -2.96996221+1.07469589e-13j -3.1852002 +3.56381591e-14j -3.11290727-4.30766534e-14j -2.75960937-1.07247544e-13j -2.15719901+9.17044218e-14j -1.36005634+2.30926389e-14j -0.44014014-5.55111512e-15j 0.51950791+1.70974346e-14j 1.4322595 -7.57172103e-14j 2.21571973-6.03961325e-14j 2.79916492+1.42108547e-14j 3.12992694+9.42024236e-14j 3.17814761-1.86073379e-13j 2.939474 -7.77156117e-14j 2.4354514 -3.10862447e-15j 1.71157838+4.13002965e-14j 0.83319962-4.66293670e-14j -0.12039283+6.88338275e-15j -1.0631173 +1.26565425e-14j -1.90987321-4.01900735e-14j -2.58422313-1.19793064e-13j -3.02529287+1.27897692e-13j -3.19326667+3.23907567e-14j -3.07298138-4.56301663e-14j -2.67529526-1.06692433e-13j -2.03610781+8.43769499e-14j -1.21311904+1.90958360e-14j -0.28062092-4.66293670e-15j 0.67720911+1.88737914e-14j 1.57390685-8.10462808e-14j 2.3285266 -6.06181771e-14j 2.87294812-2.77555756e-15j 3.158026 +9.83102488e-14j]
As can be seen, the imaginary parts are neglegible. With the highest value being on the order of $10^{-13}$:
print(np.max(np.abs(np.imag(xx))))
1.8607337892717624e-13
We had some brief talk about this before but now we'll delve on it a bit more, so how did this happen? Remember the $X_k$ coefficients? They were consisting of complex conjugates, with $X_1 = X_{249}^*$, $X_2 = X_{248}^*$, ..., $X_{12} = X_{238}^*$, ... so let's see what happens when we multiply $X_{12}$ with $e^{-i{\frac{2\pi}{250}(12)n}}$ and $X_{238}$ with $e^{-i{\frac{2\pi}{250}(238)n}}$. Before we do that, let's check $X_{12}$ and $X_{238}$ explicitly:
print(" X[12] = {:20.3f}".format(X[12]))
print("X[238] = {:20.3f}".format(X[238]))
X[12] = 1.579+0.240j X[238] = 1.579-0.240j
n = np.arange(N)
x12exp = X[12] * np.exp(-1j*2*np.pi/250*12*n)
x238exp = X[238] * np.exp(-1j*2*np.pi/250*238*n)
print("{:>3s} {:^6s} {:^20s} :: {:^20s} {:s}".\
format("i"," t(s)"," X[12][i]*exp(..)"," X[238][i]*exp(..)",\
"X[12][i]*e()+X[238][i]*e()"))
for i in n:
print("{:3d} {:6.3f} {:20.3f} :: {: 18.3f} ====> {: .3f}".\
format(i,t[i],x12exp[i],x238exp[i],(x12exp[i]+x238exp[i])/N))
i t(s) X[12][i]*exp(..) :: X[238][i]*exp(..) X[12][i]*e()+X[238][i]*e() 0 0.000 1.579+0.240j :: 1.579-0.240j ====> 0.013+0.000j 1 0.016 1.579-0.240j :: 1.579+0.240j ====> 0.013+0.000j 2 0.032 1.436-0.698j :: 1.436+0.698j ====> 0.011+0.000j 3 0.048 1.164-1.093j :: 1.164+1.093j ====> 0.009+0.000j 4 0.064 0.787-1.390j :: 0.787+1.390j ====> 0.006+0.000j 5 0.080 0.339-1.561j :: 0.339+1.561j ====> 0.003+0.000j 6 0.096 -0.140-1.591j :: -0.140+1.591j ====> -0.001-0.000j 7 0.112 -0.607-1.477j :: -0.607+1.477j ====> -0.005-0.000j 8 0.129 -1.018-1.231j :: -1.018+1.231j ====> -0.008-0.000j 9 0.145 -1.338-0.873j :: -1.338+0.873j ====> -0.011-0.000j 10 0.161 -1.536-0.436j :: -1.536+0.436j ====> -0.012-0.000j 11 0.177 -1.597+0.040j :: -1.597-0.040j ====> -0.013-0.000j 12 0.193 -1.513+0.513j :: -1.513-0.513j ====> -0.012-0.000j 13 0.209 -1.292+0.939j :: -1.292-0.939j ====> -0.010-0.000j 14 0.225 -0.955+1.280j :: -0.955-1.280j ====> -0.008-0.000j 15 0.241 -0.532+1.506j :: -0.532-1.506j ====> -0.004-0.000j 16 0.257 -0.060+1.596j :: -0.060-1.596j ====> -0.000+0.000j 17 0.273 0.417+1.542j :: 0.417-1.542j ====> 0.003+0.000j 18 0.289 0.856+1.349j :: 0.856-1.349j ====> 0.007+0.000j 19 0.305 1.218+1.033j :: 1.218-1.033j ====> 0.010+0.000j 20 0.321 1.470+0.625j :: 1.470-0.625j ====> 0.012+0.000j 21 0.337 1.589+0.160j :: 1.589-0.160j ====> 0.013+0.000j 22 0.353 1.565-0.319j :: 1.565+0.319j ====> 0.013+0.000j 23 0.369 1.400-0.769j :: 1.400+0.769j ====> 0.011+0.000j 24 0.386 1.108-1.150j :: 1.108+1.150j ====> 0.009+0.000j 25 0.402 0.716-1.428j :: 0.716+1.428j ====> 0.006+0.000j 26 0.418 0.260-1.576j :: 0.260+1.576j ====> 0.002+0.000j 27 0.434 -0.220-1.582j :: -0.220+1.582j ====> -0.002-0.000j 28 0.450 -0.680-1.445j :: -0.680+1.445j ====> -0.005-0.000j 29 0.466 -1.079-1.178j :: -1.079+1.178j ====> -0.009-0.000j 30 0.482 -1.380-0.804j :: -1.380+0.804j ====> -0.011-0.000j 31 0.498 -1.556-0.358j :: -1.556+0.358j ====> -0.012-0.000j 32 0.514 -1.593+0.120j :: -1.593-0.120j ====> -0.013-0.000j 33 0.530 -1.485+0.588j :: -1.485-0.588j ====> -0.012-0.000j 34 0.546 -1.243+1.003j :: -1.243-1.003j ====> -0.010-0.000j 35 0.562 -0.889+1.327j :: -0.889-1.327j ====> -0.007-0.000j 36 0.578 -0.455+1.531j :: -0.455-1.531j ====> -0.004-0.000j 37 0.594 0.020+1.597j :: 0.020-1.597j ====> 0.000+0.000j 38 0.610 0.494+1.519j :: 0.494-1.519j ====> 0.004+0.000j 39 0.627 0.922+1.304j :: 0.922-1.304j ====> 0.007+0.000j 40 0.643 1.268+0.971j :: 1.268-0.971j ====> 0.010+0.000j 41 0.659 1.499+0.550j :: 1.499-0.550j ====> 0.012+0.000j 42 0.675 1.595+0.080j :: 1.595-0.080j ====> 0.013+0.000j 43 0.691 1.547-0.397j :: 1.547+0.397j ====> 0.012+0.000j 44 0.707 1.359-0.839j :: 1.359+0.839j ====> 0.011+0.000j 45 0.723 1.049-1.205j :: 1.049+1.205j ====> 0.008+0.000j 46 0.739 0.643-1.462j :: 0.643+1.462j ====> 0.005+0.000j 47 0.755 0.180-1.587j :: 0.180+1.587j ====> 0.001+0.000j 48 0.771 -0.299-1.569j :: -0.299+1.569j ====> -0.002-0.000j 49 0.787 -0.752-1.409j :: -0.752+1.409j ====> -0.006-0.000j 50 0.803 -1.136-1.122j :: -1.136+1.122j ====> -0.009-0.000j 51 0.819 -1.418-0.734j :: -1.418+0.734j ====> -0.011-0.000j 52 0.835 -1.572-0.280j :: -1.572+0.280j ====> -0.013-0.000j 53 0.851 -1.585+0.200j :: -1.585-0.200j ====> -0.013-0.000j 54 0.867 -1.454+0.662j :: -1.454-0.662j ====> -0.012+0.000j 55 0.884 -1.191+1.064j :: -1.191-1.064j ====> -0.010-0.000j 56 0.900 -0.822+1.370j :: -0.822-1.370j ====> -0.007+0.000j 57 0.916 -0.378+1.552j :: -0.378-1.552j ====> -0.003+0.000j 58 0.932 0.100+1.594j :: 0.100-1.594j ====> 0.001+0.000j 59 0.948 0.569+1.492j :: 0.569-1.492j ====> 0.005+0.000j 60 0.964 0.987+1.256j :: 0.987-1.256j ====> 0.008+0.000j 61 0.980 1.315+0.906j :: 1.315-0.906j ====> 0.011+0.000j 62 0.996 1.525+0.474j :: 1.525-0.474j ====> 0.012+0.000j 63 1.012 1.597-0.000j :: 1.597+0.000j ====> 0.013+0.000j 64 1.028 1.525-0.474j :: 1.525+0.474j ====> 0.012+0.000j 65 1.044 1.315-0.906j :: 1.315+0.906j ====> 0.011+0.000j 66 1.060 0.987-1.256j :: 0.987+1.256j ====> 0.008+0.000j 67 1.076 0.569-1.492j :: 0.569+1.492j ====> 0.005+0.000j 68 1.092 0.100-1.594j :: 0.100+1.594j ====> 0.001+0.000j 69 1.108 -0.378-1.552j :: -0.378+1.552j ====> -0.003-0.000j 70 1.124 -0.822-1.370j :: -0.822+1.370j ====> -0.007-0.000j 71 1.141 -1.191-1.064j :: -1.191+1.064j ====> -0.010-0.000j 72 1.157 -1.454-0.662j :: -1.454+0.662j ====> -0.012-0.000j 73 1.173 -1.585-0.200j :: -1.585+0.200j ====> -0.013-0.000j 74 1.189 -1.572+0.280j :: -1.572-0.280j ====> -0.013-0.000j 75 1.205 -1.418+0.734j :: -1.418-0.734j ====> -0.011+0.000j 76 1.221 -1.136+1.122j :: -1.136-1.122j ====> -0.009-0.000j 77 1.237 -0.752+1.409j :: -0.752-1.409j ====> -0.006-0.000j 78 1.253 -0.299+1.569j :: -0.299-1.569j ====> -0.002+0.000j 79 1.269 0.180+1.587j :: 0.180-1.587j ====> 0.001+0.000j 80 1.285 0.643+1.462j :: 0.643-1.462j ====> 0.005+0.000j 81 1.301 1.049+1.205j :: 1.049-1.205j ====> 0.008+0.000j 82 1.317 1.359+0.839j :: 1.359-0.839j ====> 0.011+0.000j 83 1.333 1.547+0.397j :: 1.547-0.397j ====> 0.012+0.000j 84 1.349 1.595-0.080j :: 1.595+0.080j ====> 0.013+0.000j 85 1.365 1.499-0.550j :: 1.499+0.550j ====> 0.012+0.000j 86 1.382 1.268-0.971j :: 1.268+0.971j ====> 0.010+0.000j 87 1.398 0.922-1.304j :: 0.922+1.304j ====> 0.007-0.000j 88 1.414 0.494-1.519j :: 0.494+1.519j ====> 0.004+0.000j 89 1.430 0.020-1.597j :: 0.020+1.597j ====> 0.000-0.000j 90 1.446 -0.455-1.531j :: -0.455+1.531j ====> -0.004-0.000j 91 1.462 -0.889-1.327j :: -0.889+1.327j ====> -0.007+0.000j 92 1.478 -1.243-1.003j :: -1.243+1.003j ====> -0.010-0.000j 93 1.494 -1.485-0.588j :: -1.485+0.588j ====> -0.012+0.000j 94 1.510 -1.593-0.120j :: -1.593+0.120j ====> -0.013-0.000j 95 1.526 -1.556+0.358j :: -1.556-0.358j ====> -0.012+0.000j 96 1.542 -1.380+0.804j :: -1.380-0.804j ====> -0.011-0.000j 97 1.558 -1.079+1.178j :: -1.079-1.178j ====> -0.009-0.000j 98 1.574 -0.680+1.445j :: -0.680-1.445j ====> -0.005-0.000j 99 1.590 -0.220+1.582j :: -0.220-1.582j ====> -0.002+0.000j 100 1.606 0.260+1.576j :: 0.260-1.576j ====> 0.002+0.000j 101 1.622 0.716+1.428j :: 0.716-1.428j ====> 0.006+0.000j 102 1.639 1.108+1.150j :: 1.108-1.150j ====> 0.009+0.000j 103 1.655 1.400+0.769j :: 1.400-0.769j ====> 0.011+0.000j 104 1.671 1.565+0.319j :: 1.565-0.319j ====> 0.013+0.000j 105 1.687 1.589-0.160j :: 1.589+0.160j ====> 0.013+0.000j 106 1.703 1.470-0.625j :: 1.470+0.625j ====> 0.012+0.000j 107 1.719 1.218-1.033j :: 1.218+1.033j ====> 0.010+0.000j 108 1.735 0.856-1.349j :: 0.856+1.349j ====> 0.007-0.000j 109 1.751 0.417-1.542j :: 0.417+1.542j ====> 0.003+0.000j 110 1.767 -0.060-1.596j :: -0.060+1.596j ====> -0.000-0.000j 111 1.783 -0.532-1.506j :: -0.532+1.506j ====> -0.004-0.000j 112 1.799 -0.955-1.280j :: -0.955+1.280j ====> -0.008+0.000j 113 1.815 -1.292-0.939j :: -1.292+0.939j ====> -0.010-0.000j 114 1.831 -1.513-0.513j :: -1.513+0.513j ====> -0.012+0.000j 115 1.847 -1.597-0.040j :: -1.597+0.040j ====> -0.013-0.000j 116 1.863 -1.536+0.436j :: -1.536-0.436j ====> -0.012-0.000j 117 1.880 -1.338+0.873j :: -1.338-0.873j ====> -0.011-0.000j 118 1.896 -1.018+1.231j :: -1.018-1.231j ====> -0.008-0.000j 119 1.912 -0.607+1.477j :: -0.607-1.477j ====> -0.005-0.000j 120 1.928 -0.140+1.591j :: -0.140-1.591j ====> -0.001+0.000j 121 1.944 0.339+1.561j :: 0.339-1.561j ====> 0.003+0.000j 122 1.960 0.787+1.390j :: 0.787-1.390j ====> 0.006+0.000j 123 1.976 1.164+1.093j :: 1.164-1.093j ====> 0.009+0.000j 124 1.992 1.436+0.698j :: 1.436-0.698j ====> 0.011+0.000j 125 2.008 1.579+0.240j :: 1.579-0.240j ====> 0.013+0.000j 126 2.024 1.579-0.240j :: 1.579+0.240j ====> 0.013+0.000j 127 2.040 1.436-0.698j :: 1.436+0.698j ====> 0.011+0.000j 128 2.056 1.164-1.093j :: 1.164+1.093j ====> 0.009+0.000j 129 2.072 0.787-1.390j :: 0.787+1.390j ====> 0.006-0.000j 130 2.088 0.339-1.561j :: 0.339+1.561j ====> 0.003+0.000j 131 2.104 -0.140-1.591j :: -0.140+1.591j ====> -0.001-0.000j 132 2.120 -0.607-1.477j :: -0.607+1.477j ====> -0.005-0.000j 133 2.137 -1.018-1.231j :: -1.018+1.231j ====> -0.008+0.000j 134 2.153 -1.338-0.873j :: -1.338+0.873j ====> -0.011-0.000j 135 2.169 -1.536-0.436j :: -1.536+0.436j ====> -0.012-0.000j 136 2.185 -1.597+0.040j :: -1.597-0.040j ====> -0.013-0.000j 137 2.201 -1.513+0.513j :: -1.513-0.513j ====> -0.012-0.000j 138 2.217 -1.292+0.939j :: -1.292-0.939j ====> -0.010-0.000j 139 2.233 -0.955+1.280j :: -0.955-1.280j ====> -0.008-0.000j 140 2.249 -0.532+1.506j :: -0.532-1.506j ====> -0.004-0.000j 141 2.265 -0.060+1.596j :: -0.060-1.596j ====> -0.000+0.000j 142 2.281 0.417+1.542j :: 0.417-1.542j ====> 0.003+0.000j 143 2.297 0.856+1.349j :: 0.856-1.349j ====> 0.007+0.000j 144 2.313 1.218+1.033j :: 1.218-1.033j ====> 0.010+0.000j 145 2.329 1.470+0.625j :: 1.470-0.625j ====> 0.012+0.000j 146 2.345 1.589+0.160j :: 1.589-0.160j ====> 0.013+0.000j 147 2.361 1.565-0.319j :: 1.565+0.319j ====> 0.013+0.000j 148 2.378 1.400-0.769j :: 1.400+0.769j ====> 0.011+0.000j 149 2.394 1.108-1.150j :: 1.108+1.150j ====> 0.009+0.000j 150 2.410 0.716-1.428j :: 0.716+1.428j ====> 0.006-0.000j 151 2.426 0.260-1.576j :: 0.260+1.576j ====> 0.002+0.000j 152 2.442 -0.220-1.582j :: -0.220+1.582j ====> -0.002-0.000j 153 2.458 -0.680-1.445j :: -0.680+1.445j ====> -0.005-0.000j 154 2.474 -1.079-1.178j :: -1.079+1.178j ====> -0.009+0.000j 155 2.490 -1.380-0.804j :: -1.380+0.804j ====> -0.011-0.000j 156 2.506 -1.556-0.358j :: -1.556+0.358j ====> -0.012+0.000j 157 2.522 -1.593+0.120j :: -1.593-0.120j ====> -0.013-0.000j 158 2.538 -1.485+0.588j :: -1.485-0.588j ====> -0.012-0.000j 159 2.554 -1.243+1.003j :: -1.243-1.003j ====> -0.010-0.000j 160 2.570 -0.889+1.327j :: -0.889-1.327j ====> -0.007-0.000j 161 2.586 -0.455+1.531j :: -0.455-1.531j ====> -0.004-0.000j 162 2.602 0.020+1.597j :: 0.020-1.597j ====> 0.000+0.000j 163 2.618 0.494+1.519j :: 0.494-1.519j ====> 0.004+0.000j 164 2.635 0.922+1.304j :: 0.922-1.304j ====> 0.007+0.000j 165 2.651 1.268+0.971j :: 1.268-0.971j ====> 0.010+0.000j 166 2.667 1.499+0.550j :: 1.499-0.550j ====> 0.012+0.000j 167 2.683 1.595+0.080j :: 1.595-0.080j ====> 0.013+0.000j 168 2.699 1.547-0.397j :: 1.547+0.397j ====> 0.012+0.000j 169 2.715 1.359-0.839j :: 1.359+0.839j ====> 0.011+0.000j 170 2.731 1.049-1.205j :: 1.049+1.205j ====> 0.008+0.000j 171 2.747 0.643-1.462j :: 0.643+1.462j ====> 0.005-0.000j 172 2.763 0.180-1.587j :: 0.180+1.587j ====> 0.001+0.000j 173 2.779 -0.299-1.569j :: -0.299+1.569j ====> -0.002+0.000j 174 2.795 -0.752-1.409j :: -0.752+1.409j ====> -0.006+0.000j 175 2.811 -1.136-1.122j :: -1.136+1.122j ====> -0.009-0.000j 176 2.827 -1.418-0.734j :: -1.418+0.734j ====> -0.011-0.000j 177 2.843 -1.572-0.280j :: -1.572+0.280j ====> -0.013-0.000j 178 2.859 -1.585+0.200j :: -1.585-0.200j ====> -0.013+0.000j 179 2.876 -1.454+0.662j :: -1.454-0.662j ====> -0.012-0.000j 180 2.892 -1.191+1.064j :: -1.191-1.064j ====> -0.010-0.000j 181 2.908 -0.822+1.370j :: -0.822-1.370j ====> -0.007-0.000j 182 2.924 -0.378+1.552j :: -0.378-1.552j ====> -0.003+0.000j 183 2.940 0.100+1.594j :: 0.100-1.594j ====> 0.001+0.000j 184 2.956 0.569+1.492j :: 0.569-1.492j ====> 0.005+0.000j 185 2.972 0.987+1.256j :: 0.987-1.256j ====> 0.008+0.000j 186 2.988 1.315+0.906j :: 1.315-0.906j ====> 0.011-0.000j 187 3.004 1.525+0.474j :: 1.525-0.474j ====> 0.012+0.000j 188 3.020 1.597+0.000j :: 1.597+0.000j ====> 0.013+0.000j 189 3.036 1.525-0.474j :: 1.525+0.474j ====> 0.012+0.000j 190 3.052 1.315-0.906j :: 1.315+0.906j ====> 0.011-0.000j 191 3.068 0.987-1.256j :: 0.987+1.256j ====> 0.008+0.000j 192 3.084 0.569-1.492j :: 0.569+1.492j ====> 0.005+0.000j 193 3.100 0.100-1.594j :: 0.100+1.594j ====> 0.001+0.000j 194 3.116 -0.378-1.552j :: -0.378+1.552j ====> -0.003-0.000j 195 3.133 -0.822-1.370j :: -0.822+1.370j ====> -0.007+0.000j 196 3.149 -1.191-1.064j :: -1.191+1.064j ====> -0.010-0.000j 197 3.165 -1.454-0.662j :: -1.454+0.662j ====> -0.012-0.000j 198 3.181 -1.585-0.200j :: -1.585+0.200j ====> -0.013-0.000j 199 3.197 -1.572+0.280j :: -1.572-0.280j ====> -0.013+0.000j 200 3.213 -1.418+0.734j :: -1.418-0.734j ====> -0.011-0.000j 201 3.229 -1.136+1.122j :: -1.136-1.122j ====> -0.009-0.000j 202 3.245 -0.752+1.409j :: -0.752-1.409j ====> -0.006-0.000j 203 3.261 -0.299+1.569j :: -0.299-1.569j ====> -0.002+0.000j 204 3.277 0.180+1.587j :: 0.180-1.587j ====> 0.001+0.000j 205 3.293 0.643+1.462j :: 0.643-1.462j ====> 0.005+0.000j 206 3.309 1.049+1.205j :: 1.049-1.205j ====> 0.008+0.000j 207 3.325 1.359+0.839j :: 1.359-0.839j ====> 0.011-0.000j 208 3.341 1.547+0.397j :: 1.547-0.397j ====> 0.012+0.000j 209 3.357 1.595-0.080j :: 1.595+0.080j ====> 0.013+0.000j 210 3.373 1.499-0.550j :: 1.499+0.550j ====> 0.012+0.000j 211 3.390 1.268-0.971j :: 1.268+0.971j ====> 0.010-0.000j 212 3.406 0.922-1.304j :: 0.922+1.304j ====> 0.007+0.000j 213 3.422 0.494-1.519j :: 0.494+1.519j ====> 0.004+0.000j 214 3.438 0.020-1.597j :: 0.020+1.597j ====> 0.000-0.000j 215 3.454 -0.455-1.531j :: -0.455+1.531j ====> -0.004+0.000j 216 3.470 -0.889-1.327j :: -0.889+1.327j ====> -0.007+0.000j 217 3.486 -1.243-1.003j :: -1.243+1.003j ====> -0.010-0.000j 218 3.502 -1.485-0.588j :: -1.485+0.588j ====> -0.012-0.000j 219 3.518 -1.593-0.120j :: -1.593+0.120j ====> -0.013-0.000j 220 3.534 -1.556+0.358j :: -1.556-0.358j ====> -0.012+0.000j 221 3.550 -1.380+0.804j :: -1.380-0.804j ====> -0.011-0.000j 222 3.566 -1.079+1.178j :: -1.079-1.178j ====> -0.009-0.000j 223 3.582 -0.680+1.445j :: -0.680-1.445j ====> -0.005-0.000j 224 3.598 -0.220+1.582j :: -0.220-1.582j ====> -0.002+0.000j 225 3.614 0.260+1.576j :: 0.260-1.576j ====> 0.002+0.000j 226 3.631 0.716+1.428j :: 0.716-1.428j ====> 0.006+0.000j 227 3.647 1.108+1.150j :: 1.108-1.150j ====> 0.009+0.000j 228 3.663 1.400+0.769j :: 1.400-0.769j ====> 0.011-0.000j 229 3.679 1.565+0.319j :: 1.565-0.319j ====> 0.013+0.000j 230 3.695 1.589-0.160j :: 1.589+0.160j ====> 0.013+0.000j 231 3.711 1.470-0.625j :: 1.470+0.625j ====> 0.012+0.000j 232 3.727 1.218-1.033j :: 1.218+1.033j ====> 0.010-0.000j 233 3.743 0.856-1.349j :: 0.856+1.349j ====> 0.007+0.000j 234 3.759 0.417-1.542j :: 0.417+1.542j ====> 0.003+0.000j 235 3.775 -0.060-1.596j :: -0.060+1.596j ====> -0.000-0.000j 236 3.791 -0.532-1.506j :: -0.532+1.506j ====> -0.004+0.000j 237 3.807 -0.955-1.280j :: -0.955+1.280j ====> -0.008+0.000j 238 3.823 -1.292-0.939j :: -1.292+0.939j ====> -0.010-0.000j 239 3.839 -1.513-0.513j :: -1.513+0.513j ====> -0.012-0.000j 240 3.855 -1.597-0.040j :: -1.597+0.040j ====> -0.013-0.000j 241 3.871 -1.536+0.436j :: -1.536-0.436j ====> -0.012+0.000j 242 3.888 -1.338+0.873j :: -1.338-0.873j ====> -0.011-0.000j 243 3.904 -1.018+1.231j :: -1.018-1.231j ====> -0.008-0.000j 244 3.920 -0.607+1.477j :: -0.607-1.477j ====> -0.005-0.000j 245 3.936 -0.140+1.591j :: -0.140-1.591j ====> -0.001+0.000j 246 3.952 0.339+1.561j :: 0.339-1.561j ====> 0.003+0.000j 247 3.968 0.787+1.390j :: 0.787-1.390j ====> 0.006+0.000j 248 3.984 1.164+1.093j :: 1.164-1.093j ====> 0.009+0.000j 249 4.000 1.436+0.698j :: 1.436-0.698j ====> 0.011-0.000j
so you see that, the complex conjugates runs to the rescue and gets us rid of the imaginary parts therefore no problems in ignoring them:
xx = np.real(xx)
Let's compare this re-constructed $\vec {xx}$ signal with the original $\vec x$:
plt.plot(x,"b")
plt.plot(xx,"r--")
plt.legend(labels=["Original","Reconstructed"],loc="lower right")
plt.show()
It's a good match! 8)
We still need to fix the horizontal axis' meaning. Remember that, after the Fourier transformation, the indices with the highest magnitude were the 12th and the 238th (out of 250 entries). We also observed that there was a symmetrical distribution: components with the same magnitudes appeared on the both sides of the center entry #125. This is not a coincidence.
Instead of adding the contributions from the 12th and 238th entries, let's construct separate signals from each, as if only that one exists. We'll label these as xx_12 and xx_238:
n = np.arange(N)
xx_12 = X[12]*(np.cos(2*np.pi/N*12*n )+1j*np.sin(2*np.pi/N*12*n )) / N
xx_238 = X[238]*(np.cos(2*np.pi/N*238*n)+1j*np.sin(2*np.pi/N*238*n)) / N
fig,axs = plt.subplots(1,2)
axs[0].plot(np.real(xx_12 ),"b-")
axs[0].plot(np.real(xx_238),"r--")
axs[0].legend(labels=["[#12]","[#238]"],loc="upper right")
axs[1].plot(np.real(xx_12 - xx_238))
axs[1].set_title(label="Their Difference (mind the x$10^{-13}$!)")
plt.show()
They are yielding exactly the same signal (half in amplitude of the original one).
What this means is: for a real signal, the first half of the coefficients carry all the information because, the other half will be complex conjugates of them and the imaginary parts will cancel each other out when reconstructing the signal (they ought to, as our original signal is real). Due to the periodicity, we can actually shift the right part to the left of the first part and associate them with "negative" frequencies (in reality, we are just changing our focus interval from $[0,N)$ to $[-N/2,N/2)$). As for the real signals, the magnitudes of their complex conjugate components are the same, we could just reconstruct the signal by using half of the interval's coefficients and then multiplying it by 2.
Moreover, looking at the plot on the left, we see that, the signal constructed from the $k=12$ (or $k=238$ for that matters) oscillates 12 times, not surprisingly, as $\tfrac{k}{N}$ in $\left(\frac{2\pi n k}{N}\right)$ acts directly as the frequency. But our original signal's frequency wasn't 12, it was set to 3 Hz. So there must be a normalization issue, which is due to the fact that when sampling, we sampled during 4 seconds, so there were 12 oscillations per 4 seconds resulting the frequency being 3 Hz (meaning "3 oscillations per second"). We didn't feed this information to Fourier transformation: we just supplied the 250 entries' displacements without specifying how much time it took. The calculated frequency is thus unnormalized -- we must divide the horizontal scale by 4 to have it correspond to the actual scale:
ff = np.arange(N)/4
plt.plot(ff,np.abs(X))
plt.ylabel("Magnitudes")
plt.xlabel("frequencies (Hz)")
plt.show()
or if we shift the second half of the frequencies as negative frequencies (as our signal is of periodicity N, we can either take [0,𝑁)[0,N) or [−𝑁/2,𝑁/2)[−N/2,N/2) (or any other sequential 𝑁N element) intervals):
ff_re = ff.copy()
ff_re[125:] = -ff_re[125:0:-1]
print(ff_re)
[ 0. 0.25 0.5 0.75 1. 1.25 1.5 1.75 2. 2.25 2.5 2.75 3. 3.25 3.5 3.75 4. 4.25 4.5 4.75 5. 5.25 5.5 5.75 6. 6.25 6.5 6.75 7. 7.25 7.5 7.75 8. 8.25 8.5 8.75 9. 9.25 9.5 9.75 10. 10.25 10.5 10.75 11. 11.25 11.5 11.75 12. 12.25 12.5 12.75 13. 13.25 13.5 13.75 14. 14.25 14.5 14.75 15. 15.25 15.5 15.75 16. 16.25 16.5 16.75 17. 17.25 17.5 17.75 18. 18.25 18.5 18.75 19. 19.25 19.5 19.75 20. 20.25 20.5 20.75 21. 21.25 21.5 21.75 22. 22.25 22.5 22.75 23. 23.25 23.5 23.75 24. 24.25 24.5 24.75 25. 25.25 25.5 25.75 26. 26.25 26.5 26.75 27. 27.25 27.5 27.75 28. 28.25 28.5 28.75 29. 29.25 29.5 29.75 30. 30.25 30.5 30.75 31. -31.25 -31. -30.75 -30.5 -30.25 -30. -29.75 -29.5 -29.25 -29. -28.75 -28.5 -28.25 -28. -27.75 -27.5 -27.25 -27. -26.75 -26.5 -26.25 -26. -25.75 -25.5 -25.25 -25. -24.75 -24.5 -24.25 -24. -23.75 -23.5 -23.25 -23. -22.75 -22.5 -22.25 -22. -21.75 -21.5 -21.25 -21. -20.75 -20.5 -20.25 -20. -19.75 -19.5 -19.25 -19. -18.75 -18.5 -18.25 -18. -17.75 -17.5 -17.25 -17. -16.75 -16.5 -16.25 -16. -15.75 -15.5 -15.25 -15. -14.75 -14.5 -14.25 -14. -13.75 -13.5 -13.25 -13. -12.75 -12.5 -12.25 -12. -11.75 -11.5 -11.25 -11. -10.75 -10.5 -10.25 -10. -9.75 -9.5 -9.25 -9. -8.75 -8.5 -8.25 -8. -7.75 -7.5 -7.25 -7. -6.75 -6.5 -6.25 -6. -5.75 -5.5 -5.25 -5. -4.75 -4.5 -4.25 -4. -3.75 -3.5 -3.25 -3. -2.75 -2.5 -2.25 -2. -1.75 -1.5 -1.25 -1. -0.75 -0.5 -0.25]
plt.plot(ff_re,np.abs(X))
plt.ylabel("Magnitudes")
plt.xlabel("frequencies (Hz)")
plt.show()
Zooming in:
window_filter = np.abs(ff_re)<5
plt.xticks(range(-5,5))
plt.plot(ff_re[window_filter],np.abs(X)[window_filter])
plt.ylabel("Magnitudes")
plt.xlabel("frequencies (Hz)")
plt.show()
There's actually a dedicated function in numpy (and/or scipy) to make this frequency adjustment calculation for us. It is the fftfreq() function and works by inputting the number of samples along with the spacing (/distance) between them. In our case we have 250 samples and there is a space of 4 / 250 between them (the spacing is equal to the inverse of the sampling rate: we have done 250 sampling in 4 seconds so our rate is 250/4 samples/second and its inverse is 4/250), so:
ff_np = np.fft.fftfreq(250,4/250)
print(ff_np)
[ 0. 0.25 0.5 0.75 1. 1.25 1.5 1.75 2. 2.25 2.5 2.75 3. 3.25 3.5 3.75 4. 4.25 4.5 4.75 5. 5.25 5.5 5.75 6. 6.25 6.5 6.75 7. 7.25 7.5 7.75 8. 8.25 8.5 8.75 9. 9.25 9.5 9.75 10. 10.25 10.5 10.75 11. 11.25 11.5 11.75 12. 12.25 12.5 12.75 13. 13.25 13.5 13.75 14. 14.25 14.5 14.75 15. 15.25 15.5 15.75 16. 16.25 16.5 16.75 17. 17.25 17.5 17.75 18. 18.25 18.5 18.75 19. 19.25 19.5 19.75 20. 20.25 20.5 20.75 21. 21.25 21.5 21.75 22. 22.25 22.5 22.75 23. 23.25 23.5 23.75 24. 24.25 24.5 24.75 25. 25.25 25.5 25.75 26. 26.25 26.5 26.75 27. 27.25 27.5 27.75 28. 28.25 28.5 28.75 29. 29.25 29.5 29.75 30. 30.25 30.5 30.75 31. -31.25 -31. -30.75 -30.5 -30.25 -30. -29.75 -29.5 -29.25 -29. -28.75 -28.5 -28.25 -28. -27.75 -27.5 -27.25 -27. -26.75 -26.5 -26.25 -26. -25.75 -25.5 -25.25 -25. -24.75 -24.5 -24.25 -24. -23.75 -23.5 -23.25 -23. -22.75 -22.5 -22.25 -22. -21.75 -21.5 -21.25 -21. -20.75 -20.5 -20.25 -20. -19.75 -19.5 -19.25 -19. -18.75 -18.5 -18.25 -18. -17.75 -17.5 -17.25 -17. -16.75 -16.5 -16.25 -16. -15.75 -15.5 -15.25 -15. -14.75 -14.5 -14.25 -14. -13.75 -13.5 -13.25 -13. -12.75 -12.5 -12.25 -12. -11.75 -11.5 -11.25 -11. -10.75 -10.5 -10.25 -10. -9.75 -9.5 -9.25 -9. -8.75 -8.5 -8.25 -8. -7.75 -7.5 -7.25 -7. -6.75 -6.5 -6.25 -6. -5.75 -5.5 -5.25 -5. -4.75 -4.5 -4.25 -4. -3.75 -3.5 -3.25 -3. -2.75 -2.5 -2.25 -2. -1.75 -1.5 -1.25 -1. -0.75 -0.5 -0.25]
plt.plot(ff_np,np.abs(X))
plt.ylabel("Magnitudes")
plt.xlabel("frequencies (Hz)")
plt.title("Horizontal axis acquired via fftfreq")
plt.show()
Therefore, the {X_k} coefficients are actually a measure of which frequency contributes how much and once we give the real meaning to the horizontal axis, we get the precious frequency spectrum of the signal.
As we have seen and derived and proved, for a real signal, no new information is contained in the 'negative' frequency range, therefore we can just multiply the positive ones by 2 and reconstruct the same signal. So instead of the above spectrum, we have the same information below:
ffilter = ff_np>=0
plt.plot(ff_np[ffilter],2*np.abs(X[ffilter]))
plt.ylabel("Magnitudes")
plt.xlabel("frequencies (Hz)")
plt.title("Positive only frequencies domain, with the amplitudes doubled")
plt.show()
When we were talking about the horizontal axis' meaning and how to obtain the correct window with respect to the sampling rate, we easily shifted the frequencies on the right side to the negative region.
We've already saw that, when the signal is real, half of the frequencies' coefficients will be equal to the other half's coefficients' complex conjugate values. Furthermore, these corresponding pairs' ($k, N-k$) frequencies will be "inverse" to each other. (What does a 'negative frequency' mean by the way?! 8)
For the moment, let's show that waves with frequencies $\left(\frac{k}{N}\right)$ and $\left(\frac{k}{N} + mN\right)$ are equal to each other (with $\{k,N,m,n\}\in\mathbb{Z}$, $m$ arbitrary, $\{k,n\}=0\dots N-1$), so here we go:
$$\begin{align*}\cos\left[2\pi\left(\frac{k}{N} + mN\right) n\right] &= \cos\left(\frac{2\pi kn}{N} + 2\pi mNn\right)\\ &=\cos\left(\frac{2\pi kn}{N}\right)\cos\left(2\pi mNn\right)-\sin\left(\frac{2\pi kn}{N}\right)\sin\left(2\pi mNn\right) \end{align*} $$where we have used the trigonometric identity: $\cos(\alpha+\beta) \equiv \cos\alpha \cos\beta - \sin\alpha \sin\beta$. Out critical term is the $\left(2\pi mNn\right)$ and more specifically $mNn$ in there. As all $m,N,n$ are integers, that parantheses is nothing but $2\pi$ multiplied by an integer, say $a$, and we all know how that goes for cosine and sine (given $a\in\mathbb{Z}$):
$$\cos(2\pi a) = 1,\quad \sin(2\pi a) = 0\quad\quad\left(a\in\mathbb{Z}\right)$$Placing this equivalency in the above equation, we get: $$\begin{align*}\cos\left[2\pi\left(\frac{k}{N} + mN\right) n\right] &=\cos\left(\frac{2\pi kn}{N}\right)\underbrace{\cos\left(2\pi\overbrace{ mNn}^{\in\mathbb{Z}}\right)}_{1}-\sin\left(\frac{2\pi kn}{N}\right)\underbrace{\sin\left(2\pi \overbrace{ mNn}^{\in\mathbb{Z}}\right)}_{0}\\ &=\cos\left(\frac{2\pi kn}{N}\right)=\cos\left[2\pi\left(\frac{k}{N}\right)n\right] \end{align*}$$
Hence, we can add/subtract as many $N$ as we want to/from the frequency (thus shifting it) as long as we do it in integer multiplers of $N$.
When we look at it this way, maybe it isn't that impressive, however, from applicational point of view, we are boldly stating that even if the frequency is changed by a significant amount, no additional information will be gained regarding the data.
To see how this works, we will consider a very small system with $N = 5$ and $k=3$. The corresponding frequency will be: $\frac{k}{N}=\frac{3}{5}$, so let's plot it:
N = 5
k = 3
f = k/N
n = np.arange(N)
y = np.cos(2*np.pi*f*n) # These are our data
plt.plot(n,y,"bx")
plt.show()
As we know the underlying frequency, let's also put it in:
x = np.linspace(0,N-1,1000)
c = np.cos(2*np.pi*f*x)
plt.plot(x,c,"-",color="lightgreen",lw=0.8)
plt.plot(n,y,"bx")
plt.show()
Keep in mind that, we are making our calculations exactly at the integer values ($n=0,\dots,N-1$). The pale continous line (whose frequency is equal to $\tfrac{k}{N}$) is something we are not interested except at the integer parameter values.
Obviously, two waves with different frequencies will have very distinct graphs (duh!). As the frequency increases, the number of oscillations per unit length/time will also increase. So, let's plot the above wave of frequency $\tfrac{k}{N} = \tfrac{3}{5}$ along with another wave of frequency $\tfrac{k}{N}+mN = \tfrac{3}{5}+(2\times 5) = \tfrac{53}{5}$:
N = 5
k = 3
m = 2
x = np.linspace(0,N-1,1000)
f1 = k/N
f2 = k/N + m*N
c1 = np.cos(2*np.pi*f1*x)
c2 = np.cos(2*np.pi*f2*x)
plt.plot(x,c1,"-",color="lightgreen",lw=0.8)
plt.plot(x,c2,"--",color="lightgray",lw=0.8)
plt.legend(["f = 3/5","f = 53/5"],loc="upper right")
plt.show()
Nothing surprising here, as they are two waves as different as two waves with different frequencies can be! So how in the first place we could believe that there is no difference and we can take these as equivalent?!!!
The answer lies in the fact that we are only taking the integer parameters into account. Check the above graph with the points placed:
N = 5
k = 3
m = 2
x = np.linspace(0,N-1,1000)
f1 = k/N
f2 = k/N + m*N
c1 = np.cos(2*np.pi*f1*x)
c2 = np.cos(2*np.pi*f2*x)
points1 = np.cos(2*np.pi*f1*n)
points2 = np.cos(2*np.pi*f2*n)
plt.plot(x,c1,"-",color="lightgreen",lw=0.8)
plt.plot(x,c2,"--",color="lightgray",lw=0.8)
plt.plot(n,points1,"xb")
plt.plot(n,points1,"+r")
plt.legend(["f = 3/5","f = 53/5"],loc="upper right")
plt.show()
Zooming in:
plt.plot(x,c1,"-",color="lightgreen",lw=0.8)
plt.plot(x,c2,"--",color="lightgray",lw=0.8)
plt.plot(n,points1,"xb")
plt.plot(n,points1,"+r")
plt.legend(["f = 3/5","f = 13/5"],loc="upper right")
plt.xlim([0.96,3.04])
plt.show()
plt.plot(x,c1,"-",color="lightgreen",lw=0.8)
plt.plot(x,c2,"--",color="lightgray",lw=0.8)
plt.plot(n,points1,"xb")
plt.plot(n,points1,"+r")
plt.legend(["f = 3/5","f = 53/5"],loc="upper right")
plt.xlim([1.96,3.04])
plt.show()
meaning, as long as we are only interested in the integer parameters, all the $f = \tfrac{k}{N} + mN$ frequencies point to the same data, same information! (This fact is utilized beautifully in solid state physics, when we are dealing with phonons.)
To see the positive and "negative" frequencies in action, Let's start from the beginning. Let's take our sample case once again:
$$x(t) = 3.2 \cos(2\pi (3) t)$$from which we see that: Amplitude (A): 3.2 Frequency (f): 3
Now, let's try to write it as an addition of two complex waves with their amplitudes being complex conjugates of each other and also their frequencies being negative of each other:
$$\begin{align*}y_1 &= \left(a+ib\right)\left[\cos\left(2\pi (f) t\right) + i \sin\left(2\pi (f) t\right)\right]\\ y_2 &= \left(a-ib\right)\left[\cos\left(2\pi(-f) t\right) + i \sin\left(2\pi (-f) t\right)\right] \end{align*}$$Before we proceed further, please check for yourself to see that the amplitudes of the two complex waves are indeed complex conjugates of each other and also the frequencies are negative of each other as well.
Let's add these two waves. Expanding the parantheses:
$$\begin{gather*}y_1 + y_2 = \left(a+ib\right)\left[\cos\left(2\pi (f) t\right) + i \sin\left(2\pi (f) t\right)\right] + \left(a-ib\right)[\underbrace{\cos\left(2\pi(-f) t\right)}_{\cos\left(2\pi(f) t\right)} + i \,\underbrace{\sin\left(2\pi (-f) t\right)}_{-\sin\left(2\pi (f) t\right)}]\\ =\left[a\cos\left(2\pi f t\right)+ia\sin\left(2\pi f t\right)+ib\cos\left(2\pi f t\right)-b\sin\left(2\pi f t\right)\right] + \left[a\cos\left(2\pi f t\right)-ia\sin\left(2\pi f t\right)-ib\cos\left(2\pi f t\right)-b\sin\left(2\pi f t\right)\right]\\ =\boxed{2a\cos\left(2\pi f t\right) - 2b\sin\left(2\pi f t\right)} \end{gather*}$$Comparing this with the above $x(t) = 3.2 \cos(2\pi (3) t)$, it is obvious that:
If our initial pure signal was that of a sine wave, say:
$$x(t) = 3.2 \sin(2\pi (3) t)$$then, to comply, our positive and negative frequency pair would obviously be:
$$\begin{align*}y_1 &= (-1.6i)\left[\cos(2\pi(3)t) + i\sin(2\pi(3)t)\right]\\ y_2 &= (1.6i)\left[\cos(2\pi(-3)t) + i\sin(2\pi(-3)t)\right]\\&= (1.6i)\left[\cos(2\pi(3)t) - i\sin(2\pi(3)t)\right]\end{align*}$$A = 3.2
f = 3.
N = 50000
tt = np.linspace(0,4,N)
x_cos = A*np.cos(2*np.pi*f*tt)
X_cos = np.fft.fft(x_cos)/N
X_cos_a = np.abs(X_cos)
X_cos_a_normalized = X_cos_a / np.max(X_cos_a)
X_filter = X_cos_a_normalized>1E-1
fft_freq = np.fft.fftfreq(N,4/N)
print("## Cosine Wave ##")
print("Picked Frequencies: {:.3f}, {:.3f}".format(*fft_freq[X_filter]))
print("Corresponding Amplitudes: {:.3f}, {:.3f}".format(*X_cos[X_filter]))
print("-"*45)
x_sin = A*np.sin(2*np.pi*f*tt)
X_sin = np.fft.fft(x_sin)/N
X_sin_a = np.abs(X_sin)
X_sin_a_normalized = X_sin_a / np.max(X_sin_a)
X_filter = X_sin_a_normalized>1E-1
fft_freq = np.fft.fftfreq(N,4/N)
print("## Sine Wave ##")
print("Picked Frequencies: {:.3f}, {:.3f}".format(*fft_freq[X_filter]))
print("Corresponding Amplitudes: {:.3f}, {:.3f}".format(*X_sin[X_filter]))
## Cosine Wave ## Picked Frequencies: 3.000, -3.000 Corresponding Amplitudes: 1.600+0.001j, 1.600-0.001j --------------------------------------------- ## Sine Wave ## Picked Frequencies: 3.000, -3.000 Corresponding Amplitudes: 0.001-1.600j, 0.001+1.600j
In the above "Manual Transform" section, we have computed the Fourier transform using the classic formulation. However, with the advance of computing power and development of sparse matrix toolkits, a faster way $\left(O\left(N^2\right)\Rightarrow O(N \log N)\right)$) for Fourier transform has been devised in the 1960s by James Cooley and John Tukey.
We won't be dealing with how the method works but will directly benefit from it. Using it is as simple as typing fft:
# Define our signal
N = 250 # Number of samples
t = np.linspace(0,4,N)
T = 1/3 # Period
A = 3.2 # Amplitude
i2pi_N = 2j*np.pi/N
x = A * np.cos(2 * np.pi / T * t)
# Transform it via FFT
X_FFT = np.fft.fft(x)
X_FFT /= N
# Calculate the corresponding frequencies via fftfreq
ff = np.fft.fftfreq(N,4/N)
# Plot it
plt.plot(ff, np.abs(X_FFT), "b-")
plt.show()
# Compare it with the "manual" section's result
plt.plot(ff, np.abs(X_FFT), "b-")
plt.plot(ff, np.abs(X), "r--")
plt.legend(["FFT","Old School"])
plt.show()
window_filter = np.abs(ff) < 5
plt.plot(ff[window_filter], np.abs(X_FFT[window_filter]), "b-")
plt.plot(ff[window_filter], np.abs(X[window_filter]), "r--")
plt.legend(["FFT","Old School"])
plt.show()
Up until now, we had a pure sinusoidal (cosine) signal, let's pepper up the signal by combining 3 waves:
# Generate the ideal signal
N = 48000 # Number of high res "sample"
t = np.linspace(0,1,N)
freqs = [95,300,677] # Hz
amps = [0.97,3.23,2.617]
num_freqs = len(freqs)
two_pi_t = 2 * np.pi * t
x = np.zeros(N)
for i in range(num_freqs):
x += amps[i]*np.cos(two_pi_t*freqs[i])
# Plot
time_interval_window = [0.2,0.225]
n1 = np.arange(N)[t>=time_interval_window[0]][0]
n2 = np.arange(N)[t>=time_interval_window[1]][0]
plt.plot(t[n1:n2],x[n1:n2],"b-")
plt.show()
# FFT decomposition
xt = np.fft.fft(x)
xt = xt[:int(N/2)] # remove the symmetrical frequencies
xt_abs = np.abs(xt)
# Fix the amplitudes
xt_abs = xt_abs/xt.size
# Filter the weak components
xt_norm = xt_abs / np.max(xt_abs)
freqs_recon = []
for i in np.arange(int(N/2))[xt_norm>0.25]:
print("{:3d} : {:.3f}".format(i,xt_abs[i]))
freqs_recon.append(i)
95 : 0.970 300 : 3.230 677 : 2.616
freq_upto = 1000
plt.plot(range(freq_upto),xt_abs[:freq_upto])
plt.show()
So, there we have it, exactly as given. However, we did it with a high number of samples ($N=48000$). Which brings us to the next lesson (again)...
The amplitudes are accurate when the number of samples are high. Let's quickly re-do the above processes this time with a lower N:
# Generate the ideal signal
n_low = 2000 # Number of low res "sample"
t_low = np.linspace(0,1,n_low)
freqs = [95,300,677] # Hz
amps = [0.97,3.23,2.61]
num_freqs = len(freqs)
two_pi_t_low = 2 * np.pi * t_low
x_low = np.zeros(n_low)
for i in range(num_freqs):
x_low += amps[i]*np.cos(two_pi_t_low*freqs[i])
# Plot
time_interval_window = [0.2,0.225]
n1 = np.arange(n_low)[t_low>=time_interval_window[0]][0]
n2 = np.arange(n_low)[t_low>=time_interval_window[1]][0]
plt.plot(t_low[n1:n2],x_low[n1:n2],"b-")
plt.show()
# FFT decomposition
xt_low = np.fft.fft(x_low)
xt_low = xt_low[:int(n_low/2)] # remove the symmetrical frequencies
xt_abs_low = np.abs(xt_low)
# Fix the amplitudes
xt_abs_low = xt_abs_low/xt_low.size
# Filter the weak components
xt_norm_low = xt_abs_low / np.max(xt_abs_low)
freqs_recon_low = []
for i in np.arange(int(n_low/2))[xt_norm_low>0.25]:
print("{:3d} : {:.3f}".format(i,xt_abs_low[i]))
freqs_recon_low.append(i)
freq_upto_low = 1000
plt.plot(range(freq_upto_low),xt_abs_low[:freq_upto_low])
plt.show()
95 : 0.973 300 : 3.116 677 : 2.145 678 : 1.098
as can be seen there is a splitting of the high frequency. (677, but also 678)
Finding the amplitudes (only cosines case)
Now that we have the frequencies pinned down, it is more or less straightforward to fine tune the amplitudes via least squares fitting method (or any other optimization algorithm). We have 3 unknown amplitudes $\vec{A} = \{A^{(0)}, A^{(1)}, A^{(2)}\}$ for the three frequencies $\{f^{(0)}, f^{(1)}, f^{(2)}\}$ and $n$ equations such that:
$$\begin{align*}A^{(0)}\cos(2\pi f^{(0)}t_0) + A^{(1)}\cos(2\pi f^{(1)}t_0) + A^{(2)}\cos(2\pi f^{(2)}t_0) &= x_0\\ \vdots \\ A^{(0)}\cos(2\pi f^{(0)}t_i) + A^{(1)}\cos(2\pi f^{(1)}t_i) + A^{(2)}\cos(2\pi f^{(2)}t_i) &= x_i\\ \vdots \\ A^{(0)}\cos(2\pi f^{(0)}t_{n-1}) + A^{(1)}\cos(2\pi f^{(1)}t_{n-1}) + A^{(2)}\cos(2\pi f^{(2)}t_{n-1}) &= x_{n-1} \end{align*}$$which can be written as a matrix multiplication:
$$\underbrace{\begin{pmatrix}\cos(2\pi f^{(0)}t_0) && \cos(2\pi f^{(1)}t_0) && \cos(2\pi f^{(2)}t_0)\\ \vdots&&\vdots&&\vdots\\ \cos(2\pi f^{(0)}t_{n-1}) && \cos(2\pi f^{(1)}t_{n-1}) && \cos(2\pi f^{(2)}t_{n-1})\end{pmatrix}_{(n\times3)}}_{\mathbb{C}} \underbrace{\begin{pmatrix}A^{(0)}\\A^{(1)}\\A^{(2)}\end{pmatrix}_{(3\times1)}}_{\vec{A}} = \underbrace{\begin{pmatrix}x_0\\\vdots \\x_i\\\vdots \\x_{n-1}\end{pmatrix}_{(n\times1)}}_{\vec{x}} $$$${\mathbb{C}}\cdot\vec{A}=\vec{x}$$This is an overdetermined set of linear equations (3 unknowns and n>3 equations), so least squares fitting is a suitable choice of attack:
C=np.empty((0,t.size))
for f in freqs_recon:
C = np.vstack([C,np.cos(two_pi_t*f)])
C = C.T
print(C)
[[1. 1. 1. ] [0.99992268 0.999229 0.99607573] [0.99969072 0.99691721 0.98433374] ... [0.99969072 0.99691721 0.98433374] [0.99992268 0.999229 0.99607573] [1. 1. 1. ]]
amps_recon = np.linalg.lstsq(C,x,rcond=None)[0]
print(("{:.3f} | "*amps_recon.size).format(*amps_recon))
0.970 | 3.230 | 2.617 |
Mind though, this working out the amplitudes straightforward is not a general approach, it worked here because there were only cosines involved. But no worries, before the lecture ends, we'll have a general approach (involving least squares fitting) in our hands.
Moral of the story: Always eyeball the calculations, make your estimations before you see the results and don't leave it all to computer, because the computer always returns a result.
Secondary -scientific- moral of the story: The frequencies are the essential targets - once you find them, correct amplitudes can be derived easily, so, focus on the frequencies, and the rest will follow.
When we filtered out the "weak" frequencies in the spectrum above (the xt_norm_low>0.25 line), we started something new: we based our reasoning that the signal was composed of a couple of "strong" signals and the rest was "noise". So, let's add some actual random noise to our signal:
# Noise
nz = 2*(np.random.rand(N)*2-1)
x_nz = x + nz
# Plot
time_interval_window = [0.2,0.205]
n1 = np.arange(N)[t>=time_interval_window[0]][0]
n2 = np.arange(N)[t>=time_interval_window[1]][0]
plt.plot(t[n1:n2],x_nz[n1:n2],"b-")
plt.show()
Fourier transform reveals the active frequencies:
# FFT decomposition
xt_nz = np.fft.fft(x_nz)
xt_nz = xt_nz[:int(N/2)] # remove the symmetrical frequencies
xt_abs_nz = np.abs(xt_nz)
# Fix the amplitudes
xt_abs_nz = xt_abs_nz/xt_nz.size
# Filter the weak components
xt_norm_nz = xt_abs_nz / np.max(xt_abs_nz)
freqs_recon_nz = []
for i in np.arange(int(N/2))[xt_norm_nz>0.25]:
print("{:3d} : {:.3f}".format(i,xt_abs_nz[i]))
freqs_recon_nz.append(i)
# Remove the noise frequencies
xt_abs_nz[xt_norm_nz<0.25] = 0.
freq_upto = 1000
plt.plot(range(freq_upto),xt_abs_nz[:freq_upto])
plt.show()
95 : 0.962 300 : 3.230 677 : 2.640
# Recover the pure signal by constructing from the filtered frequencies
xt_nz_filtered = np.fft.fft(x_nz)
xt_nz_filtered_abs = np.abs(xt_nz_filtered)
xt_nz_filtered[xt_nz_filtered_abs/np.max(xt_nz_filtered_abs)<0.25] = 0.
x_recon = np.real(np.fft.ifft(xt_nz_filtered))
# Plot
time_interval_window = [0.2,0.205]
n1 = np.arange(N)[t>=time_interval_window[0]][0]
n2 = np.arange(N)[t>=time_interval_window[1]][0]
plt.plot(t[n1:n2],x_nz[n1:n2],"r-")
plt.plot(t[n1:n2],x_recon[n1:n2],"b-",linewidth=3)
plt.plot(t[n1:n2],x[n1:n2],"g--",linewidth=3)
plt.legend(["noisy","filtered","pure"])
plt.show()
Job done!
So far, we've dealt with a single, pure cosine function, now let's build a signal consisting of multiple functions with different amplitudes and frequencies:
N = 4000
freqs = [200, 343, 576, 900]
amps = [1.2, 0.97, 1.13, 1.17]
funcs = [np.cos, np.cos, np.sin, np.cos]
t = np.linspace(0,2,N)
two_pi_t = 2*np.pi*t
signal = np.zeros(N,float)
for i in range(len(freqs)):
signal += amps[i] * funcs[i](two_pi_t*freqs[i])
plt.plot(t,signal,color="lightgray",linewidth=1)
plt.show()
So, this is our signal, in all its glory. Our "recording" duration is 2 seconds, with 4000 samples (we have forgotten its constituents: as of now we don't know the involved functions, their frequencies, their amplitudes, not even how many there are).
Zooming in, it looks like this:
time_window = [0.50,0.60]
t_filter = (t>=time_window[0]) & (t<=time_window[1])
plt.plot(t[t_filter], signal[t_filter])
plt.show()
So, let's apply FFT to the signal:
signal_fft = np.fft.fft(signal)
ff = np.fft.fftfreq(N,2/N)
plt.plot(ff, np.abs(signal_fft))
plt.show()
Pick the peaks from the positive range:
peak_indices_filter = np.abs(signal_fft)>1000
freqs_recon_all = ff[peak_indices_filter]
freqs_recon_positive = freqs_recon_all[freqs_recon_all>0]
print(freqs_recon_positive)
indices_picked = []
for i in np.arange(N)[peak_indices_filter]:
if(ff[i]>0):
indices_picked.append(i)
print("[#{:4d}] {:6.2f} : {:7.3f}".format(i,ff[i],
2*np.abs(signal_fft)[i]/N))
indices_picked
[200. 343. 576. 900. 900.5] [# 400] 200.00 : 1.182 [# 686] 343.00 : 0.924 [#1152] 576.00 : 0.982 [#1800] 900.00 : 0.818 [#1801] 900.50 : 0.668
[400, 686, 1152, 1800, 1801]
Instead of problem-specific filtering with amplitudes greater than 1000, a general solution of filtering above 50% of the normalized amplitudes can be used as well. In order to do so, we normalize the amplitudes and take the ones that are greater than 0.5 (i.e., > 50%) (We have already did it like this in the above cases, and this is the reason why we did it in that way: to generalize the code)
signal_fft_abs = np.abs(signal_fft)
signal_fft_abs_normalized = signal_fft_abs / np.max(signal_fft_abs)
f_norm_filter = signal_fft_abs_normalized>0.5
ffreq = np.fft.fftfreq(N,2/N)
ffreq_pos_filter = ffreq > 0
f_filter = (f_norm_filter) & (ffreq_pos_filter)
indices_picked = np.arange(N)[f_filter]
print(indices_picked)
[ 400 686 1152 1800 1801]
The frequencies are caught very well except for the smearing of the highest frequency. This kind of "splittings" may occur near the high frequency range with a low number of samples (as we've seen and investigated in the "The importance of the number of samples (in regard to frequency splittings)" section above).
We have the frequencies, have the amplitudes and also can reconstruct the signal using the inverse fourier transform (mind how we multiply the coefficients by 2 to also count for the negative frequency contributions)
amps_picked = signal_fft[indices_picked]*2/N
signal_recon = np.zeros_like(signal_fft)
for i in np.arange(int(N/2)):
amp = signal_fft[i]/N*2
#print(ffreq[i])
if(i not in indices_picked):
continue
signal_recon += amp*np.exp(1j*i/N*2*np.pi*np.arange(N))
plt.plot(t,signal,"b-")
plt.plot(t,np.real(signal_recon),"r-")
plt.show()
time_window = [0.50,0.60]
t_filter = (t>=time_window[0]) & (t<=time_window[1])
plt.plot(t[t_filter], signal[t_filter],"b-")
plt.plot(t[t_filter], np.real(signal_recon[t_filter]),"r-")
plt.show()
plt.plot(t,np.abs(signal - np.real(signal_recon)),"-",
color="lightgray")
plt.title("Calculated Error")
plt.show()
as can be seen, the errors are significantly higher on the edges. One way to remedy -as we had demonstrated in the "The importance of the number of samples (in regard to frequency splittings)" section, is to increase the number of samples. To see this, just increase the $N$ parameter at the beginning of this section (say 10 fold) and re-calculate - here it is:
N10 = 4000*10
freqs = [200, 343, 576, 900]
amps = [1.2, 0.97, 1.13, 1.17]
funcs = [np.cos, np.cos, np.sin, np.cos]
t10 = np.linspace(0,2,N10)
two_pi_t10 = 2*np.pi*t10
signal10 = np.zeros(N10,float)
for i in range(len(freqs)):
signal10 += amps[i] * funcs[i](two_pi_t10*freqs[i])
signal_fft10 = np.fft.fft(signal10)
ff10 = np.fft.fftfreq(N10,2/N10)
signal_fft_abs10 = np.abs(signal_fft10)
signal_fft_abs_normalized10 = signal_fft_abs10 / np.max(signal_fft_abs10)
f_norm_filter10 = signal_fft_abs_normalized10>0.5
ffreq10 = np.fft.fftfreq(N10,2/N10)
ffreq_pos_filter10 = ffreq10 > 0
f_filter10 = (f_norm_filter10) & (ffreq_pos_filter10)
indices_picked10 = np.arange(N10)[f_filter10]
print(indices_picked10)
amps_picked10 = signal_fft10[indices_picked10]*2/N10
signal_recon10 = np.zeros_like(signal_fft10)
for i in np.arange(int(N10/2)):
amp10 = signal_fft10[i]/N10*2
if(i not in indices_picked10):
continue
signal_recon10 += amp10*np.exp(1j*i/N10*2*np.pi*np.arange(N10))
plt.plot(t10,signal10,"b-")
plt.plot(t10,np.real(signal_recon10),"r-")
plt.show()
time_window = [0.50,0.60]
t_filter10 = (t10>=time_window[0]) & (t10<=time_window[1])
plt.plot(t10[t_filter10], signal10[t_filter10],"b-")
plt.plot(t10[t_filter10], np.real(signal_recon10[t_filter10]),"r-")
plt.show()
plt.plot(t10,np.abs(signal10 - np.real(signal_recon10)),"-",
color="lightgray")
plt.title("Calculated Error")
plt.show()
[ 400 686 1152 1800]
But what happens if we do not have the opportunity to increase the number of samples (e.g., when we have already made our observations and it would be too impractical to re-do the measurements)? Then, we "use our heads" 8)
Let's go once again to our relatively low number of samples and take it from there: Remember that we had filtered out the frequencies with normalized amplitudes less than 0.5? Let's decrease that threshold to 0.1 and then try to reconstruct the original signal:
N = 4000
freqs = [200, 343, 576, 900]
amps = [1.2, 0.97, 1.13, 1.17]
funcs = [np.cos, np.cos, np.sin, np.cos]
threshold = 0.1
t = np.linspace(0,2,N)
two_pi_t = 2*np.pi*t
signal = np.zeros(N,float)
for i in range(len(freqs)):
signal += amps[i] * funcs[i](two_pi_t*freqs[i])
signal_fft = np.fft.fft(signal)
ff = np.fft.fftfreq(N,2/N)
signal_fft_abs = np.abs(signal_fft)
signal_fft_abs_normalized = signal_fft_abs / np.max(signal_fft_abs)
f_norm_filter = signal_fft_abs_normalized>threshold
ffreq = np.fft.fftfreq(N,2/N)
ffreq_pos_filter = ffreq > 0
f_filter = (f_norm_filter) & (ffreq_pos_filter)
indices_picked = np.arange(N)[f_filter]
print("Indices Picked:")
print(indices_picked)
print("Frequencies Picked:")
print(ffreq[f_filter])
amps_picked = signal_fft[indices_picked]*2/N
signal_recon = np.zeros_like(signal_fft)
for i in np.arange(int(N/2)):
amp = signal_fft[i]/N*2
#print(ffreq[i])
if(i not in indices_picked):
continue
signal_recon += amp*np.exp(1j*i/N*2*np.pi*np.arange(N))
plt.plot(t,signal,"b-")
plt.plot(t,np.real(signal_recon),"r-")
plt.show()
time_window = [0.50,0.60]
t_filter = (t>=time_window[0]) & (t<=time_window[1])
plt.plot(t[t_filter], signal[t_filter],"b-")
plt.plot(t[t_filter], np.real(signal_recon[t_filter]),"r-")
plt.show()
plt.plot(t,np.abs(signal - np.real(signal_recon)),"-",
color="lightgray")
plt.title("Calculated Error")
plt.show()
Indices Picked: [ 400 401 685 686 687 1150 1151 1152 1153 1154 1798 1799 1800 1801 1802 1803] Frequencies Picked: [200. 200.5 342.5 343. 343.5 575. 575.5 576. 576.5 577. 899. 899.5 900. 900.5 901. 901.5]
Really much better indeed! Although by including all the "side" frequencies, we are actually losing the simplicity of the real signal. Even though the mathematical result is better, the physical point of view got much complicated. (This is the part where we'll start using our heads! 8)
Let's remember the frequencies and their corresponding amplitudes when we went for 50% amplitude filtering:
peak_indices_filter = np.abs(signal_fft)>1000
freqs_recon_all = ff[peak_indices_filter]
freqs_recon_positive = freqs_recon_all[freqs_recon_all>0]
print(freqs_recon_positive)
indices_picked = []
amps_picked = []
for i in np.arange(N)[peak_indices_filter]:
if(ff[i]>0):
indices_picked.append(i)
amps_picked.append(2*signal_fft[i]/N)
print("[#{:4d}] {:6.2f} : {:7.3f} | {:.3f}".format(i,ff[i],
2*np.abs(signal_fft)[i]/N,amps_picked[-1]))
[200. 343. 576. 900. 900.5] [# 400] 200.00 : 1.182 | 1.124+0.365j [# 686] 343.00 : 0.924 | 0.794+0.474j [#1152] 576.00 : 0.982 | 0.772-0.606j [#1800] 900.00 : 0.818 | 0.127+0.808j [#1801] 900.50 : 0.668 | -0.105-0.660j
The amplitudes are not that well spotted but now that we have the frequencies, they can be very easily be fitted (using a method like least squares fit - in a minute).
Speaking of frequencies, we see that the obtained frequencies of 900 and 900.5 are too close to be a coincidence, therefore we'll keep the 900 and leave 900.5. This leaves us with 4 frequencies (and even though we "don't know" it (as we had made ourselves forgotten once we built the original signal from them), they are the exact frequencies we had used). We have seen that, with the calculated amplitudes they yielded not-so-great results, therefore, we'll play with the amplitudes using the least-squares fitting method:
# Ditch the 900.5 frequency from our picked list
freqs_picked = np.delete(freqs_recon_positive,-1)
amps_picked = np.delete(amps_picked,-1)
print(freqs_picked)
print(amps_picked)
[200. 343. 576. 900.] [1.12405272+0.36505635j 0.79352649+0.47388979j 0.77234153-0.60601437j 0.12723789+0.8082015j ]
At the moment, our summed up function looks like this:
print("signal = ")
for i in np.arange(freqs_picked.size):
amp_cos = amps_picked[i].real
amp_sin = amps_picked[i].imag
print("+",end="") if (i) else ""
print(" "*5,"2*{:.3f}*cos(2*pi*({:.1f})t)".format(amp_cos,freqs_picked[i]),end="")
print(" - 2*{:.3f}*sin(2*pi*({:.1f})t)".format(amp_sin,freqs_picked[i]))
signal =
2*1.124*cos(2*pi*(200.0)t) - 2*0.365*sin(2*pi*(200.0)t)
+ 2*0.794*cos(2*pi*(343.0)t) - 2*0.474*sin(2*pi*(343.0)t)
+ 2*0.772*cos(2*pi*(576.0)t) - 2*-0.606*sin(2*pi*(576.0)t)
+ 2*0.127*cos(2*pi*(900.0)t) - 2*0.808*sin(2*pi*(900.0)t)
and how different (/erroneous) is it from the original signal? Let's find out, but for practical purposes, let's define a function that takes these amplitudes as parameters (in the form of an array) and returns the difference:
def fun_diff_sincos(A):
sig_con = np.zeros_like(signal)
for i in np.arange(freqs_picked.size):
amp_cos = A[2*i]
amp_sin = A[2*i+1]
sig_con += amp_cos*np.cos(2*np.pi*freqs_picked[i]*t)
sig_con -= amp_sin*np.sin(2*np.pi*freqs_picked[i]*t)
return sig_con - signal
or, alternatively (& equivalently), using the exponential form:
def fun_diff_exp(A):
sig_con = np.zeros_like(signal,complex)
for i in np.arange(freqs_picked.size):
AA = A[2*i] + 1j*A[2*i+1]
sig_con += AA * np.exp(1j*2*np.pi*freqs_picked[i]*t)
return sig_con.real - signal
and here it is in action:
amps_real = amps_picked.real
amps_imag = amps_picked.imag
A0 = np.vstack((amps_real,amps_imag)).T.flatten()
print(A0)
diff = fun_diff_sincos(A0)
plt.plot(t,diff)
plt.show()
[ 1.12405272 0.36505635 0.79352649 0.47388979 0.77234153 -0.60601437 0.12723789 0.8082015 ]
A0 is the array (/vector) that holds the amplitudes we had from the FFT, those will play a role as our starting point. We have included the real and imaginary parts as separate entries as most of the optimization functions are not supporting direct processing of complex parameters.
Our aim is to adjust the amplitudes vector to yield the minimum return (error) from our difference function. For this purpose we will use the least squares fitting method:
from scipy.optimize import least_squares
A_opt = least_squares(fun_diff_sincos,A0).x
print(A_opt)
# exponential representation:
#A_opt_exp = least_squares(fun_diff_exp,A0).x
#print(A_opt_exp)
[ 1.20000000e+00 -8.49300505e-14 9.70000000e-01 -1.76198929e-13 -2.60012306e-15 -1.13000000e+00 1.17000000e+00 1.13894769e-13]
Getting rid of zeros (<1E-10):
A_opt[np.abs(A_opt)<1E-10] = 0
print(A_opt)
[ 1.2 0. 0.97 0. 0. -1.13 1.17 0. ]
Compare these with the input parameters:
amps
[1.2, 0.97, 1.13, 1.17]
At this moment, you might be wondering why did we implemented the sin part to the function as
sig_con -= amp_sin*np.sin(2*np.pi*freqs_picked[i]*t) and also -directly as a result of that- got the amplitude as -1.13, instead of our input of 1.13. The reason lies in the expansion of the amplitudes in the complex form - the details are given and derived in the "One Last Remark" section above, but the summary is the equivalency of:
so, the amplitudes of the sine components come as subtraction! ;) (Mind that, when we have a signal made of pure cosines and sines, the magnitude of the amplitudes are the absolute values of the coefficients, hence the positive amplitudes: a complex coefficient like $0-1.13i$ has a magnitude of $1.13$). To see this more clearly, imagine that we only had a sine wave as the signal, say $1.3\sin(2\pi(576)t)$: Fourier transform would pick positive and negative frequencies in the form of:
$$(a + ib)[\cos(2\pi(576)t) + i\sin(2\pi(576)t)] \\+ (a - ib)[\cos(2\pi(-576)t) + i \sin(2\pi(-576)t)] $$and we would equate this to our input signal of $1.3\sin(2\pi(576)t)$. Expanding the complex coefficient over the $\cos(\dots) + i \sin(\dots)$ and also using $\cos(-\theta)=\cos(\theta),\;\sin(-\theta)=-\sin(\theta)$ the addition of the positive and negative frequencies contributions would be:
$$2a\cos(2\pi(576)t) - 2b\sin(2\pi(576)t)$$and equating it to our original wave, we'd have:
$$2a\cos(2\pi(576)t) - 2b\sin(2\pi(576)t) = 1.3\sin(2\pi(576)t)$$From here, we see that $a=0$ while $-2b = 1.3$ hence the coefficient $2b$ is found out to be: $2b = -1.3$.
Let's rebuilt the signal from the obtained ones:
A_complex = np.zeros(4,complex)
signal_re = np.zeros_like(signal,complex)
for i in np.arange(freqs_picked.size):
A_complex[i] = A_opt[2*i] + 1j*A_opt[2*i+1]
signal_re += A_complex[i] * np.exp(1j*2*np.pi*freqs_picked[i]*t)
print(A_complex)
plt.plot(t,signal,"b-")
plt.plot(t,signal_re.real,"r-")
plt.show()
time_window = [0.50,0.60]
t_filter = (t>=time_window[0]) & (t<=time_window[1])
plt.plot(t[t_filter], signal[t_filter],"b-")
plt.plot(t[t_filter], signal_re[t_filter].real,"r-")
plt.show()
plt.plot(t,fun_diff_sincos(A_opt),"-",color="lightgray")
plt.title("Calculated Error (Mind the $10^{-12}$ factor!)")
plt.show()
[1.2 +0.j 0.97+0.j 0. -1.13j 1.17+0.j ]
and there we have it! <dance! dance! dance!>