#!/usr/bin/env python3 # vim: fdm=marker fmr=<<<,>>> __doc__ = \ """ Create one/two figures exemplifiying the fourier transform of noisy sine wave. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import scipy.fft as ft rng = np.random.default_rng() def fft_spectrum( signal, sample_rate, fft=None, freq=None): N_samples = len(signal) real_signal = np.isrealobj(signal) if fft is None: if real_signal: fft = ft.rfft freq = ft.rfftfreq else: fft = ft.fft freq = ft.fftfreq if freq is None: freq = ft.fftfreq spectrum = fft(signal) / len(signal) if real_signal: spectrum *= 2 freqs = freq(N_samples, 1/sample_rate) return spectrum, freqs def dtft_spectrum(signal, time, freqs): freqtime = np.outer(freqs, time) # determine all coefficients in one go c_k = 2*np.cos(2*np.pi*freqtime) / len(signal) s_k = -2*np.sin(2*np.pi*freqtime) / len(signal) # dot product gives the dtft result = np.dot(c_k, signal) + 1j*np.dot(s_k, signal) return result, freqs ## From https: def multiple_formatter(denominator=2, number=np.pi, latex='\pi'): def gcd(a, b): while b: a, b = b, a%b return a def _multiple_formatter(x, pos): den = denominator num = int(np.rint(den*x/number)) com = gcd(num,den) (num,den) = (int(num/com),int(den/com)) if den==1: if num==0: return r'$0$' if num==1: return r'$%s$'%latex elif num==-1: return r'$-%s$'%latex else: return r'$%s%s$'%(num,latex) else: if num==1: return r'$\frac{%s}{%s}$'%(latex,den) elif num==-1: return r'$\frac{-%s}{%s}$'%(latex,den) else: return r'$\frac{%s%s}{%s}$'%(num,latex,den) return _multiple_formatter # def phase_plot(ax, phase_on_xaxis=False, limits=(-1*np.pi, +1*np.pi), major_ticks_div=1, minor_ticks_div=4): set_label = ax.set_ylabel set_lims = ax.set_ylim axis = ax.yaxis if phase_on_xaxis: set_label = ax.set_xlabel set_lims = ax.set_ylim axis = ax.xaxis set_label("Phase") if limits is not None: set_lims(*limits) ax.margins(y=0.2) if major_ticks_div: axis.set_major_locator(plt.MultipleLocator(np.pi / major_ticks_div)) if minor_ticks_div: axis.set_minor_locator(plt.MultipleLocator(np.pi / minor_ticks_div)) axis.set_major_formatter(plt.FuncFormatter(multiple_formatter())) return ax def main( f_sine=0.05153, # GHz noise_sigma = 0.5, sine_amplitude = 1, phase=0.4, ): t_fine = np.linspace(0, 200, 500) # ns t_sampled = np.linspace(0, 200, 50) # ns sine_fine = sine_amplitude * np.cos( 2*np.pi * f_sine * t_fine + phase) sine_sampled = sine_amplitude * np.cos( 2*np.pi * f_sine * t_sampled + phase) combined = noise_sigma*rng.normal(size=len(sine_sampled)) + sine_sampled figs = [] # time domain if True: fig, ax = plt.subplots() ax.set_xlabel("Time [ns]") ax.set_ylabel("Amplitude") ax.plot(t_fine, sine_fine, label="Clean Signal") ax.plot(t_sampled, combined, label="+ Noise", marker='o') ax.legend() ax.grid() figs.append(ax.get_figure()) # frequency domain if True: fft_spec, fft_freqs = fft_spectrum(combined, 1/(t_sampled[1] - t_sampled[0])) dtft_freqs = np.linspace(fft_freqs[0], fft_freqs[-1], 500) dtft_spec, dtft_freqs = dtft_spectrum(combined, t_sampled, dtft_freqs) fig2 = plt.figure() gs = gridspec.GridSpec(2, 1, figure=fig2, height_ratios=[3,1], hspace=0) ax1 = fig2.add_subplot(gs[:-1, -1]) ax2 = fig2.add_subplot(gs[-1, -1], sharex=ax1) axes = np.array([ax1, ax2]) ax2.set_xlabel("Frequency [GHz]") if True: dtft_freqs *= 1e3 fft_freqs *= 1e3 f_sine *= 1e3 ax2.set_xlabel("Frequency [MHz]") ax1.xaxis.tick_top() [label.set_visible(False) for label in ax1.get_xticklabels()] # indicate f_sine for ax in axes: ax.axvline(f_sine, label="$f_\mathrm{sin}$", ls='dashed', color='red') # plot amplitudes ax1.set_ylabel("Amplitude") ax1.set_ylim(0, None) ax1.yaxis.get_major_ticks()[0].set_visible(False)# suppress 0.0 ax1.grid() ax1.plot(fft_freqs, np.abs(fft_spec), marker='o', label='DFT') ax1.plot(dtft_freqs, np.sqrt(np.abs(dtft_spec)**2), label='DTFT') ax1.legend() # plot phases phase_plot(ax2, limits=[-1.1*np.pi, +1.1*np.pi]) ax2.grid() ax2.plot(fft_freqs, np.angle(fft_spec), marker='o', label='DFT') ax2.plot(dtft_freqs, np.angle(dtft_spec), label='DTFT') figs.append(fig2) return figs if __name__ == "__main__": from argparse import ArgumentParser import os.path as path import os import sys # Append parent directory to import path so pyfiglib can be found sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) import pyfiglib as pfl parser = ArgumentParser(description=__doc__) parser.add_argument("fname", metavar="path/to/figure[/]", nargs="*", help="Location for generated figure, will append __file__ if a directory. If not supplied, figure is shown.", default=os.getcwd()) args = parser.parse_args() default_extensions = ['pdf', 'png'] default_names = ['waveforms', 'spectrum'] if args.fname == 'none': args.fname = None pfl.rcParams['font.size'] = 20 pfl.rcParams['figure.figsize'] = (8,6) pfl.rcParams['figure.constrained_layout.use'] = True ### figs = main() ### Save or show figures if not args.fname: # empty list, False, None plt.show() else: for i, f in enumerate(figs): if len(args.fname) == 1 and len(figs) != 1: for ext in default_extensions: f.savefig(path.join(args.fname[0], default_names[i]) + "." + ext, transparent=True) else: f.savefig(args.fname[i], transparent=True)