from typing import Tuple, List, Union import numpy as np from scipy.signal import spectrogram from matplotlib import pyplot as plt def calculate_gradient_spectrum( obj, max_frequency: float = 2000, window_width: float = 0.05, frequency_oversampling: float = 3, time_range: Union[List[float], None] = None, plot: bool = True, combine_mode: str = 'max', use_derivative: bool = False, acoustic_resonances: List[dict] = [], ) -> Tuple[List[np.ndarray], np.ndarray, np.ndarray, np.ndarray]: """ Calculates the gradient spectrum of the sequence. Returns a spectrogram for each gradient channel, as well as a root-sum-squares combined spectrogram. Works by splitting the sequence into windows that are 'window_width' long and calculating the fourier transform of each window. Windows overlap 50% with the previous and next window. When 'combine_mode' is not 'none', all windows are combined into one spectrogram. Parameters ---------- max_frequency : float, optional Maximum frequency to include in spectrograms. The default is 2000. window_width : float, optional Window width (in seconds). The default is 0.05. frequency_oversampling : float, optional Oversampling in the frequency dimension, higher values make smoother spectrograms. The default is 3. time_range : List[float], optional Time range over which to calculate the spectrograms as a list of two timepoints (in seconds) (e.g. [1, 1.5]) The default is None. plot : bool, optional Whether to plot the spectograms. The default is True. combine_mode : str, optional How to combine all windows into one spectrogram, options: 'max', 'mean', 'rss' (root-sum-of-squares), 'none' (no combination) The default is 'max'. use_derivative : bool, optional Whether the use the derivative of the gradient waveforms instead of the gradient waveforms for the gradient spectrum calculations. The default is False acoustic_resonances : List[dict], optional Acoustic resonances as a list of dictionaries with 'frequency' and 'bandwidth' elements. Only used when plot==True. The default is []. Returns ------- spectrograms : List[np.ndarray] List of spectrograms per gradient channel. spectrogram_rss : np.ndarray Root-sum-of-squares combined spectrogram over all gradient channels. frequencies : np.ndarray Frequency axis of the spectrograms. times : np.ndarray Time axis of the spectrograms (only relevant when combine_mode == 'none'). """ dt = obj.system.grad_raster_time # time raster nwin = round(window_width / dt) nfft = round(frequency_oversampling*nwin) # Get gradients as piecewise-polynomials gw_pp = obj.get_gradients(time_range=time_range) ng = len(gw_pp) max_t = max(g.x[-1] for g in gw_pp if g is not None) # Determine sampling points if time_range == None: nt = int(np.ceil(max_t/dt)) t = (np.arange(nt) + 0.5)*dt else: tmax = min(time_range[1], max_t) - max(time_range[0], 0) nt = int(np.ceil(tmax/dt)) t = max(time_range[0], 0) + (np.arange(nt) + 0.5)*dt # Sample gradients gw = np.zeros((ng,t.shape[0])) for i in range(ng): if gw_pp[i] != None: gw[i] = gw_pp[i](t) if use_derivative: gw = np.diff(gw, axis=1) # Calculate spectrogram for each gradient channel spectrograms: List[np.ndarray] = [] spectrogram_rss = 0 for i in range(ng): # Use scipy to calculate the spectrograms freq, times, sxx = spectrogram(gw[i], fs=1/dt, mode='magnitude', nperseg=nwin, noverlap=nwin//2, nfft=nfft, detrend='constant', window=('tukey', 1)) mask = freq