When interpolating a sine wave in Python, it seems I get a lot of additional frequencies closely around the fundamental. Why is that the case? I would have expected a more or less similar spectrum, with images of the original spectrum at higher frequencies. How can I get a "clean" interpolated signal? I tried zero-order hold and linear interpolation, however that didn't seem to affect those frequencies around the fundamental.
import numpy as np
import math
import matplotlib.pyplot as plt
from scipy.fftpack import fft
from scipy import interpolate
M = 32 # Oversampling
M_interp = 16
f_in = 1e6
amp_in = 1
f_s = f_in * M
f_s_interp = f_s * M_interp
N = int((f_s/f_in) * 1024) # Nr of samples
N_interp = N * M_interp
ax_t = np.linspace(0, float((1 / f_s) * (N - 1)), num=int(N), dtype=float)
ax_t_interp = np.linspace(0, float((1 / f_s) * (N - 1)), num=int(N_interp), dtype=float)
ax_f = np.fft.fftfreq(N, 1 / f_s)[0:N // 2]
ax_f_interp = np.fft.fftfreq(N_interp, 1 / f_s_interp)[0:N_interp // 2]
x = amp_in * np.cos(2 * math.pi * f_in * ax_t)
f_int_zero = interpolate.interp1d(ax_t, x, kind='zero')
f_int_lin = interpolate.interp1d(ax_t, x, kind='linear')
y_int_zero = f_int_zero(ax_t_interp)
y_int_lin = f_int_lin(ax_t_interp)
plt.step(ax_t, x, where='post', linewidth=2, label ='Sine')
plt.step(ax_t_interp, y_int_zero, linewidth=2, label ='Sine Interp Zero')
plt.step(ax_t_interp, y_int_lin, linewidth=2, label ='Sine Interp Lin')
plt.xlim(0, 1.25e-6)
plt.legend(loc='upper right', shadow=True, fontsize=12)
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.title('Time Domain')
plt.figure()
plt.scatter(ax_f, 20np.log10(np.divide(np.abs(fft(x)), N/2))[0:N // 2], label = 'FFT Sine', marker='x', s = 80)
plt.scatter(ax_f_interp, 20 np.log10(np.divide(np.abs(fft(y_int_zero)), N_interp / 2))[0:N_interp // 2], label ='FFT Sine Zero Interp', marker='')
plt.scatter(ax_f_interp, 20 np.log10(np.divide(np.abs(fft(y_int_lin)), N_interp / 2))[0:N_interp // 2], label ='FFT Sine Lin Interp', marker='*')
plt.ylim(-100, 5)
plt.xlim(0, 2.5e6)
plt.legend(loc='upper right', shadow=True, fontsize=12)
plt.xlabel('Frequency [Hz]')
plt.ylabel('Magnitude')
plt.title('Freq Domain')
plt.show()
UPDATE: I coded the zero-order hold myself as shown below, which eliminated the frequencies closely around the fundamental.
y_int_zero = np.zeros(len(ax_t_interp))
for i in range(len(ax_t_interp)):
sample_index = int(i / M_interp)
y_int_zero[i] = x[sample_index]


