In [29]:
import numpy as np
import matplotlib.pyplot as plt
from scipy.fftpack import fft
In [30]:
%%javascript
IPython.OutputArea.prototype._should_scroll = function(lines) { return false; }
In [32]:
n = 12
T = 7.
tg = np.linspace(0,T,n,endpoint=False) # grid points
t  = np.linspace(0,T,400)  # simulate real interval
twopi = 2*np.pi
I = complex(0,1)
for k in range(0,n):  # first let k range from 0 to n-1
    plt.figure(figsize=(10,2))
    X  = np.exp(twopi*I*k*t /T)
    xg = np.exp(twopi*I*k*tg/T)
    plt.plot(tg,np.real(xg),'ro',markersize=10)  # real part red
    plt.plot(t ,np.real(X ),'r')
    plt.plot(tg,np.imag(xg),'bo')  # imaginary part blue
    plt.plot(t ,np.imag(X ),'b')
    plt.title(f'k = {k}')

Notice that the high-k modes (k > n/2) plotted above have oscillations that make them a very unnatural interpolant to the grid data.

Let us try a different range of k values:

In [35]:
n = 12
T = 7.
tg = np.linspace(0,T,n,endpoint=False) # grid points
t  = np.linspace(0,T,400)  # simulate real interval
twopi = 2*np.pi
I = complex(0,1)
for k in range(-n//2,n//2-1):
    plt.figure(figsize=(10,2))
    X  = np.exp(twopi*I*k*t /T)
    xg = np.exp(twopi*I*k*tg/T)
    plt.plot(tg,np.real(xg),'ro',markersize=10)  # real part red
    plt.plot(t ,np.real(X ),'r')
    plt.plot(tg,np.imag(xg),'bo')  # imaginary part blue
    plt.plot(t ,np.imag(X ),'b')
    plt.title(f'k = {k}')

This looks much better: we have a basis that has |k| limited to n/2: no gratuitous high-frequency oscillation.

Now let's see whether if we sample these basis functions on the grid we recover the basis function as the interpolant:

In [42]:
T = 7.
tg = np.linspace(0,T,n,endpoint=False) # grid points
t  = np.linspace(0,T,400)  # simulate real interval
twopi = 2*np.pi
I = complex(0,1)

k = 3
# choose one of the basis functions as the function to be sampled
offset = 0.1 # for more generic function to be sampled
xg = np.exp(twopi*I*k*(tg-offset)/T)
X  = np.exp(twopi*I*k*(t -offset)/T)
# construct the interpolant
y = fft(xg)/np.sqrt(n)  # dft of xg
# now construct the interpolant
P = np.zeros_like(X)

kvalues = range(-n//2,n//2)   # this time let k run from -n/2 to n/2-1
for k in kvalues:
    # add the term for the kth mode
    term = np.exp(twopi*I*k*t/T)*y[k%n]/np.sqrt(n) # k%n to fold k into range 0 .. n-1
    P += term
    
plt.plot(t,np.real(X),'m',lw=10,alpha=0.25,label='sampled')
plt.plot(tg,np.real(xg),'ro',label='grid sample')
plt.plot(t,np.real(P),'r',label='interpolant');

plt.plot(t ,np.imag(X),'c',lw=10,alpha=0.25)
plt.plot(tg,np.imag(xg),'bo')
plt.plot(t ,np.imag(P),'b')
plt.legend();

The answer is yes, except for a function with frequency at or above the Nyquist frequency |k| = n/2: where we get something else:

In [46]:
k = n//2
# choose one of the basis functions as the function to be sampled
offset = 0.1 # for more generic function to be sampled
xg = np.exp(twopi*I*k*(tg-offset)/T)
X  = np.exp(twopi*I*k*(t -offset)/T)
# construct the interpolant
y = fft(xg)/np.sqrt(n)  # dft of xg
# now construct the interpolant
P = np.zeros_like(X)

kvalues = range(-n//2,n//2)
for k in kvalues:
    # add the term for the kth mode
    term = np.exp(twopi*I*k*t/T)*y[k%n]/np.sqrt(n) # k%n to fold k into range 0 .. n-1
    P += term

plt.plot(t,np.real(X),'m',lw=5,alpha=0.25,label='sampled')
plt.plot(tg,np.real(xg),'ro',label='grid sample')
plt.plot(t,np.real(P),'r',label='interpolant');

plt.plot(t ,np.imag(X),'c',lw=5,alpha=0.25)
plt.plot(tg,np.imag(xg),'bo')
plt.plot(t ,np.imag(P),'b')
plt.legend();

This can be fixed, in the sense of getting a reasonable interpolant, by including the average of the k=-n/2 and k=n/2 terms:

In [48]:
k = n//2
# choose one of the basis functions as the function to be sampled
offset = 0.1 # for more generic function to be sampled
xg = np.exp(twopi*I*k*(tg-offset)/T)
X  = np.exp(twopi*I*k*(t -offset)/T)
# construct the interpolant
y = fft(xg)/np.sqrt(n)  # dft of xg
# now construct the interpolant
P = np.zeros_like(X)

kvalues = range(-n//2+1,n//2)
for k in kvalues:
    # add the term for the kth mode
    term = np.exp(twopi*I*k*t/T)*y[k%n]/np.sqrt(n) # k%n to fold k into range 0 .. n-1
    P += term
# deal with Nyquist freq: take average of -n/2 term and n/2 term
for k in [-n//2,n//2]:
    term = np.exp(twopi*I*k*t/T)*y[k%n]/np.sqrt(n) # k%n to fold k into range 0 .. n-1
    P += term/2
    
plt.plot(t,np.real(X),'m',lw=5,alpha=0.25,label='sampled')
plt.plot(tg,np.real(xg),'ro',label='grid sample')
plt.plot(t,np.real(P),'r',label='interpolant');

plt.plot(t ,np.imag(X),'c',lw=5,alpha=0.25)
plt.plot(tg,np.imag(xg),'bo')
plt.plot(t ,np.imag(P),'b')
plt.legend();

The above interpolant is a sensible |k|=n/2 interpolant to the grid data.

In [ ]: