Esempio n. 1
0
def test_filter(resp_wfm,
                frequency_filters=None,
                noise_threshold=None,
                excit_wfm=None,
                show_plots=True,
                plot_title=None,
                verbose=False):
    """
    Filters the provided response with the provided filters.
    Parameters
    ----------
    resp_wfm : array-like, 1D
        Raw response waveform in the time domain
    frequency_filters : (Optional) FrequencyFilter object or list of
        Frequency filters to apply to signal
    noise_threshold : (Optional) float
        Noise threshold to apply to signal
    excit_wfm : (Optional) 1D array-like
        Excitation waveform in the time domain. This waveform is necessary for plotting loops. If the length of
        resp_wfm matches excit_wfm, a single plot will be returned with the raw and filtered signals plotted against the
        excit_wfm. Else, resp_wfm and the filtered (filt_data) signal will be broken into chunks matching the length of
        excit_wfm and a figure with multiple plots (one for each chunk) with the raw and filtered signal chunks plotted
        against excit_wfm will be returned for fig_loops
    show_plots : (Optional) Boolean
        Whether or not to plot FFTs before and after filtering
    plot_title : str / unicode (Optional)
        Title for the raw vs filtered plots if requested. For example - 'Row 15'
    verbose : (Optional) Boolean
        Prints extra debugging information if True.  Default False
    Returns
    -------
    filt_data : 1D numpy float array
        Filtered signal in the time domain
    fig_fft : matplotlib.pyplot.figure object
        handle to the plotted figure if requested, else None
    fig_loops : matplotlib.pyplot.figure object
        handle to figure with the filtered signal and raw signal plotted against the excitation waveform
    """
    if not isinstance(resp_wfm, (np.ndarray, list)):
        raise TypeError('resp_wfm should be array-like')
    resp_wfm = np.array(resp_wfm)

    show_loops = False
    if excit_wfm is not None and show_plots:
        if len(resp_wfm) % len(excit_wfm) == 0:
            show_loops = True
        else:
            raise ValueError(
                'Length of resp_wfm should be divisibe by length of excit_wfm')
    if show_loops:
        if plot_title is None:
            plot_title = 'FFT Filtering'
        else:
            assert isinstance(plot_title, (str, unicode))

    if frequency_filters is None and noise_threshold is None:
        raise ValueError(
            'Need to specify at least some noise thresholding / frequency filter'
        )

    if noise_threshold is not None:
        if noise_threshold >= 1 or noise_threshold <= 0:
            raise ValueError('Noise threshold must be within (0 1)')

    samp_rate = 1
    composite_filter = 1
    if frequency_filters is not None:
        if not isinstance(frequency_filters, Iterable):
            frequency_filters = [frequency_filters]
        if not are_compatible_filters(frequency_filters):
            raise ValueError(
                'frequency filters must be a single or list of FrequencyFilter objects'
            )
        composite_filter = build_composite_freq_filter(frequency_filters)
        samp_rate = frequency_filters[0].samp_rate

    resp_wfm = np.array(resp_wfm)
    num_pts = resp_wfm.size

    fft_pix_data = np.fft.fftshift(np.fft.fft(resp_wfm))

    if noise_threshold is not None:
        noise_floor = get_noise_floor(fft_pix_data, noise_threshold)[0]
        if verbose:
            print('The noise_floor is', noise_floor)

    fig_fft = None
    if show_plots:
        w_vec = np.linspace(-0.5 * samp_rate, 0.5 * samp_rate, num_pts) * 1E-3

        fig_fft, [ax_raw, ax_filt] = plt.subplots(figsize=(12, 8), nrows=2)
        axes_fft = [ax_raw, ax_filt]
        set_tick_font_size(axes_fft, 14)

        r_ind = num_pts
        if isinstance(composite_filter, np.ndarray):
            r_ind = np.max(np.where(composite_filter > 0)[0])

        x_lims = slice(len(w_vec) // 2, r_ind)
        amp = np.abs(fft_pix_data)
        ax_raw.semilogy(w_vec[x_lims], amp[x_lims], label='Raw')
        if frequency_filters is not None:
            ax_raw.semilogy(w_vec[x_lims],
                            (composite_filter[x_lims] + np.min(amp)) *
                            (np.max(amp) - np.min(amp)),
                            linewidth=3,
                            color='orange',
                            label='Composite Filter')
        if noise_threshold is not None:
            ax_raw.axhline(
                noise_floor,
                # ax_raw.semilogy(w_vec, np.ones(r_ind - l_ind) * noise_floor,
                linewidth=2,
                color='r',
                label='Noise Threshold')
        ax_raw.legend(loc='best', fontsize=14)
        ax_raw.set_title('Raw Signal', fontsize=16)
        ax_raw.set_ylabel('Magnitude (a. u.)', fontsize=14)

    fft_pix_data *= composite_filter

    if noise_threshold is not None:
        fft_pix_data[
            np.abs(fft_pix_data) <
            noise_floor] = 1E-16  # DON'T use 0 here. ipython kernel dies

    if show_plots:
        ax_filt.semilogy(w_vec[x_lims], np.abs(fft_pix_data)[x_lims])
        ax_filt.set_title('Filtered Signal', fontsize=16)
        ax_filt.set_xlabel('Frequency(kHz)', fontsize=14)
        ax_filt.set_ylabel('Magnitude (a. u.)', fontsize=14)
        if noise_threshold is not None:
            ax_filt.set_ylim(
                bottom=noise_floor
            )  # prevents the noise threshold from messing up plots
        fig_fft.tight_layout()

    filt_data = np.real(np.fft.ifft(np.fft.ifftshift(fft_pix_data)))

    if verbose:
        print('The shape of the filtered data is {}'.format(filt_data.shape))
        print('The shape of the excitation waveform is {}'.format(
            excit_wfm.shape))

    fig_loops = None
    if show_loops:
        if len(resp_wfm) == len(excit_wfm):
            # single plot:
            fig_loops, axis = plt.subplots(figsize=(5.5, 5))
            axis.plot(excit_wfm, resp_wfm, 'r', label='Raw')
            axis.plot(excit_wfm, filt_data, 'k', label='Filtered')
            axis.legend(fontsize=14)
            set_tick_font_size(axis, 14)
            axis.set_xlabel('Excitation', fontsize=16)
            axis.set_ylabel('Signal', fontsize=16)
            axis.set_title(plot_title, fontsize=16)
            fig_loops.tight_layout()
        else:
            # N loops:
            raw_pixels = np.reshape(resp_wfm, (-1, len(excit_wfm)))
            filt_pixels = np.reshape(filt_data, (-1, len(excit_wfm)))
            print(raw_pixels.shape, filt_pixels.shape)

            fig_loops, axes_loops = plot_curves(
                excit_wfm, [raw_pixels, filt_pixels],
                line_colors=['r', 'k'],
                dataset_names=['Raw', 'Filtered'],
                x_label='Excitation',
                y_label='Signal',
                subtitle_prefix='Col ',
                num_plots=16,
                title=plot_title)

    return filt_data, fig_fft, fig_loops
Esempio n. 2
0
def plot_bayesian_results(bias_sine, i_meas, i_corrected, bias_triang, resistance, r_variance, i_recon=None,
                          pix_pos=[0, 0], broken_resistance=True, r_max=None, res_scatter=False, **kwargs):
    """
    Plots the basic Bayesian Inference results for a specific pixel
    Parameters
    ----------
    bias_sine : 1D float numpy array
        Original bias vector used for experiment
    i_meas : 1D float numpy array
        Current measured from experiment
    i_corrected : 1D float numpy array
        current with capacitance and R extra compensated
    i_recon : 1D float numpy array
        Reconstructed current
    bias_triang : 1D float numpy array
        Interpolated bias
    resistance : 1D float numpy array
        Inferred resistance
    r_variance : 1D float numpy array
        Variance of the resistance
    pix_pos : list of two numbers
        Pixel row and column positions or values
    broken_resistance : bool, Optional
        Whether or not to break the resistance plots into sections so as to avoid plotting areas with high variance
    r_max : float, Optional
        Maximum value of resistance to plot
    res_scatter : bool, Optional
        Use scatter instead of line plots for resistance
    Returns
    -------
    fig : matplotlib.pyplot figure handle
        Handle to figure
    """

    font_size_1 = 14
    font_size_2 = 16

    half_x_ind = int(0.5 * bias_triang.size)

    ex_amp = np.max(bias_triang)

    colors = [['red', 'orange'], ['blue', 'cyan']]
    syms = [['-', '--', '--'], ['-', ':', ':']]
    names = ['Forward', 'Reverse']

    cos_omega_t = np.roll(bias_sine, int(-0.25 * bias_sine.size))
    orig_half_pt = int(0.5 * bias_sine.size)
    i_correct_rolled = np.roll(i_corrected, int(-0.25 * bias_sine.size))

    st_dev = np.sqrt(r_variance)
    tests = [st_dev < 10, resistance > 0]
    if r_max is not None:
        tests.append(resistance < r_max)
    good_pts = np.ones(resistance.shape, dtype=bool)
    for item in tests:
        good_pts = np.logical_and(good_pts, item)
    good_pts = np.where(good_pts)[0]
    good_forw = good_pts[np.where(good_pts < half_x_ind)[0]]
    good_rev = good_pts[np.where(good_pts >= half_x_ind)[0]]
    pos_limits = resistance + st_dev
    neg_limits = resistance - st_dev

    fig, axes = plt.subplots(ncols=3, figsize=(15, 5))
    # fig.subplots_adjust(wspace=3.5)

    axes[0].set_ylabel('Resistance (G$\Omega$)', fontsize=font_size_2)

    pts_to_plot = [good_forw, good_rev]

    for type_ind, axis, pts_list, cols_set, sym_set, set_name in zip(range(len(names)),
                                                                     axes[:2], pts_to_plot,
                                                                     colors, syms, names):
        axis.set_title('$R(V)$ ' + set_name + ' at Row = ' + str(pix_pos[1]) +
                       ' Col =' + str(pix_pos[0]), fontsize=font_size_2)

        single_plot = not broken_resistance
        if broken_resistance:
            diff = np.diff(pts_list)
            jump_inds = np.argwhere(diff > 4) + 1
            if jump_inds.size < 1:
                single_plot = True

        if not single_plot:
            jump_inds = np.append(np.append(0, jump_inds), pts_list[-1])
            for ind in range(1, jump_inds.size):
                cur_range = pts_list[jump_inds[ind - 1]:jump_inds[ind]]
                if res_scatter:
                    axis.scatter(bias_triang[cur_range], resistance[cur_range],
                                 color=cols_set[0], s=30)
                else:
                    axis.plot(bias_triang[cur_range], resistance[cur_range], cols_set[0],
                              linestyle=sym_set[0], linewidth=3)
                axis.fill_between(bias_triang[cur_range], pos_limits[cur_range], neg_limits[cur_range],
                                  alpha=0.25, color=cols_set[1])
                if ind == 1:
                    axis.legend(['R(V)', 'R(V)+-$\sigma$'], loc='upper center', fontsize=font_size_1)
        else:
            if res_scatter:
                axis.scatter(bias_triang[pts_list], resistance[pts_list],
                             color=cols_set[0], s=30)
            else:
                axis.plot(bias_triang[pts_list], resistance[pts_list], cols_set[0],
                          linestyle=sym_set[0], linewidth=3, label='R(V)')
            axis.fill_between(bias_triang[pts_list], pos_limits[pts_list], neg_limits[pts_list],
                              alpha=0.25, color=cols_set[1], label='R(V)+-$\sigma$')
            axis.legend(loc='upper center', fontsize=font_size_1)
        axis.set_xlabel('Voltage (V)', fontsize=font_size_2)

        axis.set_xlim((-ex_amp, ex_amp))

    # ################### CURRENT PLOT ##########################

    axes[2].plot(bias_sine, i_meas, 'green', linewidth=3, label='I$_{meas}$')
    if i_recon is not None:
        axes[2].plot(bias_sine, i_recon, 'c--', linewidth=3, label='I$_{recon}$')
    axes[2].plot(cos_omega_t[orig_half_pt:], i_correct_rolled[orig_half_pt:],
                 'blue', linewidth=3, label='I$_{Bayes} Forw$')
    axes[2].plot(cos_omega_t[:orig_half_pt], i_correct_rolled[:orig_half_pt],
                 'red', linewidth=3, label='I$_{Bayes} Rev$')

    # axes[2].legend(loc='upper right', bbox_to_anchor=(-.1, 0.30), fontsize=font_size_1)
    axes[2].legend(loc='best', fontsize=font_size_1)
    axes[2].set_xlabel('Voltage(V)', fontsize=font_size_2)
    axes[2].set_title('$I(V)$ at row ' + str(pix_pos[0]) + ', col ' + str(pix_pos[1]),
                      fontsize=font_size_2)

    axes[2].set_ylabel('Current (nA)', fontsize=font_size_2)

    set_tick_font_size(axes, font_size_1)

    fig.tight_layout()

    return fig
Esempio n. 3
0
def plot_bayesian_results(bias_sine,
                          i_meas,
                          i_corrected,
                          bias_triang,
                          resistance,
                          r_variance,
                          i_recon=None,
                          pix_pos=[0, 0],
                          broken_resistance=True,
                          r_max=None,
                          res_scatter=False,
                          **kwargs):
    """
    Plots the basic Bayesian Inference results for a specific pixel
    Parameters
    ----------
    bias_sine : 1D float numpy array
        Original bias vector used for experiment
    i_meas : 1D float numpy array
        Current measured from experiment
    i_corrected : 1D float numpy array
        current with capacitance and R extra compensated
    i_recon : 1D float numpy array
        Reconstructed current
    bias_triang : 1D float numpy array
        Interpolated bias
    resistance : 1D float numpy array
        Inferred resistance
    r_variance : 1D float numpy array
        Variance of the resistance
    pix_pos : list of two numbers
        Pixel row and column positions or values
    broken_resistance : bool, Optional
        Whether or not to break the resistance plots into sections so as to avoid plotting areas with high variance
    r_max : float, Optional
        Maximum value of resistance to plot
    res_scatter : bool, Optional
        Use scatter instead of line plots for resistance
    Returns
    -------
    fig : matplotlib.pyplot figure handle
        Handle to figure
    """

    font_size_1 = 14
    font_size_2 = 16

    half_x_ind = int(0.5 * bias_triang.size)

    ex_amp = np.max(bias_triang)

    colors = [['red', 'orange'], ['blue', 'cyan']]
    syms = [['-', '--', '--'], ['-', ':', ':']]
    names = ['Forward', 'Reverse']

    cos_omega_t = np.roll(bias_sine, int(-0.25 * bias_sine.size))
    orig_half_pt = int(0.5 * bias_sine.size)
    i_correct_rolled = np.roll(i_corrected, int(-0.25 * bias_sine.size))

    st_dev = np.sqrt(r_variance)
    tests = [st_dev < 10, resistance > 0]
    if r_max is not None:
        tests.append(resistance < r_max)
    good_pts = np.ones(resistance.shape, dtype=bool)
    for item in tests:
        good_pts = np.logical_and(good_pts, item)
    good_pts = np.where(good_pts)[0]
    good_forw = good_pts[np.where(good_pts < half_x_ind)[0]]
    good_rev = good_pts[np.where(good_pts >= half_x_ind)[0]]
    pos_limits = resistance + st_dev
    neg_limits = resistance - st_dev

    fig, axes = plt.subplots(ncols=3, figsize=(15, 5))
    # fig.subplots_adjust(wspace=3.5)

    axes[0].set_ylabel('Resistance (G$\Omega$)', fontsize=font_size_2)

    pts_to_plot = [good_forw, good_rev]

    for type_ind, axis, pts_list, cols_set, sym_set, set_name in zip(
            range(len(names)), axes[:2], pts_to_plot, colors, syms, names):
        axis.set_title('$R(V)$ ' + set_name + ' at Row = ' + str(pix_pos[1]) +
                       ' Col =' + str(pix_pos[0]),
                       fontsize=font_size_2)

        single_plot = not broken_resistance
        if broken_resistance:
            diff = np.diff(pts_list)
            jump_inds = np.argwhere(diff > 4) + 1
            if jump_inds.size < 1:
                single_plot = True

        if not single_plot:
            jump_inds = np.append(np.append(0, jump_inds), pts_list[-1])
            for ind in range(1, jump_inds.size):
                cur_range = pts_list[jump_inds[ind - 1]:jump_inds[ind]]
                if res_scatter:
                    axis.scatter(bias_triang[cur_range],
                                 resistance[cur_range],
                                 color=cols_set[0],
                                 s=30)
                else:
                    axis.plot(bias_triang[cur_range],
                              resistance[cur_range],
                              cols_set[0],
                              linestyle=sym_set[0],
                              linewidth=3)
                axis.fill_between(bias_triang[cur_range],
                                  pos_limits[cur_range],
                                  neg_limits[cur_range],
                                  alpha=0.25,
                                  color=cols_set[1])
                if ind == 1:
                    axis.legend(['R(V)', 'R(V)+-$\sigma$'],
                                loc='upper center',
                                fontsize=font_size_1)
        else:
            if res_scatter:
                axis.scatter(bias_triang[pts_list],
                             resistance[pts_list],
                             color=cols_set[0],
                             s=30)
            else:
                axis.plot(bias_triang[pts_list],
                          resistance[pts_list],
                          cols_set[0],
                          linestyle=sym_set[0],
                          linewidth=3,
                          label='R(V)')
            axis.fill_between(bias_triang[pts_list],
                              pos_limits[pts_list],
                              neg_limits[pts_list],
                              alpha=0.25,
                              color=cols_set[1],
                              label='R(V)+-$\sigma$')
            axis.legend(loc='upper center', fontsize=font_size_1)
        axis.set_xlabel('Voltage (V)', fontsize=font_size_2)

        axis.set_xlim((-ex_amp, ex_amp))

    # ################### CURRENT PLOT ##########################

    axes[2].plot(bias_sine, i_meas, 'green', linewidth=3, label='I$_{meas}$')
    if i_recon is not None:
        axes[2].plot(bias_sine,
                     i_recon,
                     'c--',
                     linewidth=3,
                     label='I$_{recon}$')
    axes[2].plot(cos_omega_t[orig_half_pt:],
                 i_correct_rolled[orig_half_pt:],
                 'blue',
                 linewidth=3,
                 label='I$_{Bayes} Forw$')
    axes[2].plot(cos_omega_t[:orig_half_pt],
                 i_correct_rolled[:orig_half_pt],
                 'red',
                 linewidth=3,
                 label='I$_{Bayes} Rev$')

    # axes[2].legend(loc='upper right', bbox_to_anchor=(-.1, 0.30), fontsize=font_size_1)
    axes[2].legend(loc='best', fontsize=font_size_1)
    axes[2].set_xlabel('Voltage(V)', fontsize=font_size_2)
    axes[2].set_title('$I(V)$ at row ' + str(pix_pos[0]) + ', col ' +
                      str(pix_pos[1]),
                      fontsize=font_size_2)

    axes[2].set_ylabel('Current (nA)', fontsize=font_size_2)

    set_tick_font_size(axes, font_size_1)

    fig.tight_layout()

    return fig
Esempio n. 4
0
def test_filter(resp_wfm, frequency_filters=None, noise_threshold=None, excit_wfm=None, show_plots=True,
                plot_title=None, verbose=False):
    """
    Filters the provided response with the provided filters.

    Parameters
    ----------
    resp_wfm : array-like, 1D
        Raw response waveform in the time domain
    frequency_filters : (Optional) FrequencyFilter object or list of
        Frequency filters to apply to signal
    noise_threshold : (Optional) float
        Noise threshold to apply to signal
    excit_wfm : (Optional) 1D array-like
        Excitation waveform in the time domain. This waveform is necessary for plotting loops. If the length of
        resp_wfm matches excit_wfm, a single plot will be returned with the raw and filtered signals plotted against the
        excit_wfm. Else, resp_wfm and the filtered (filt_data) signal will be broken into chunks matching the length of
        excit_wfm and a figure with multiple plots (one for each chunk) with the raw and filtered signal chunks plotted
        against excit_wfm will be returned for fig_loops
    show_plots : (Optional) Boolean
        Whether or not to plot FFTs before and after filtering
    plot_title : str / unicode (Optional)
        Title for the raw vs filtered plots if requested. For example - 'Row 15'
    verbose : (Optional) Boolean
        Prints extra debugging information if True.  Default False

    Returns
    -------
    filt_data : 1D numpy float array
        Filtered signal in the time domain
    fig_fft : matplotlib.pyplot.figure object
        handle to the plotted figure if requested, else None
    fig_loops : matplotlib.pyplot.figure object
        handle to figure with the filtered signal and raw signal plotted against the excitation waveform
    """
    if not isinstance(resp_wfm, (np.ndarray, list)):
        raise TypeError('resp_wfm should be array-like')
    resp_wfm = np.array(resp_wfm)

    show_loops = False
    if excit_wfm is not None and show_plots:
        if len(resp_wfm) % len(excit_wfm) == 0:
            show_loops = True
        else:
            raise ValueError('Length of resp_wfm should be divisibe by length of excit_wfm')
    if show_loops:
        if plot_title is None:
            plot_title = 'FFT Filtering'
        else:
            assert isinstance(plot_title, (str, unicode))

    if frequency_filters is None and noise_threshold is None:
        raise ValueError('Need to specify at least some noise thresholding / frequency filter')

    if noise_threshold is not None:
        if noise_threshold >= 1 or noise_threshold <= 0:
            raise ValueError('Noise threshold must be within (0 1)')

    samp_rate = 1
    composite_filter = 1
    if frequency_filters is not None:
        if not isinstance(frequency_filters, Iterable):
            frequency_filters = [frequency_filters]
        if not are_compatible_filters(frequency_filters):
            raise ValueError('frequency filters must be a single or list of FrequencyFilter objects')
        composite_filter = build_composite_freq_filter(frequency_filters)
        samp_rate = frequency_filters[0].samp_rate

    resp_wfm = np.array(resp_wfm)
    num_pts = resp_wfm.size

    fft_pix_data = np.fft.fftshift(np.fft.fft(resp_wfm))

    if noise_threshold is not None:
        noise_floor = get_noise_floor(fft_pix_data, noise_threshold)[0]
        if verbose:
            print('The noise_floor is', noise_floor)

    fig_fft = None
    if show_plots:
        w_vec = np.linspace(-0.5 * samp_rate, 0.5 * samp_rate, num_pts) * 1E-3

        fig_fft, [ax_raw, ax_filt] = plt.subplots(figsize=(12, 8), nrows=2)
        axes_fft = [ax_raw, ax_filt]
        set_tick_font_size(axes_fft, 14)

        r_ind = num_pts
        if isinstance(composite_filter, np.ndarray):
            r_ind = np.max(np.where(composite_filter > 0)[0])

        x_lims = slice(len(w_vec) // 2, r_ind)
        amp = np.abs(fft_pix_data)
        ax_raw.semilogy(w_vec[x_lims], amp[x_lims], label='Raw')
        if frequency_filters is not None:
            ax_raw.semilogy(w_vec[x_lims], (composite_filter[x_lims] + np.min(amp)) * (np.max(amp) - np.min(amp)),
                            linewidth=3, color='orange', label='Composite Filter')
        if noise_threshold is not None:
            ax_raw.axhline(noise_floor,
                           # ax_raw.semilogy(w_vec, np.ones(r_ind - l_ind) * noise_floor,
                           linewidth=2, color='r', label='Noise Threshold')
        ax_raw.legend(loc='best', fontsize=14)
        ax_raw.set_title('Raw Signal', fontsize=16)
        ax_raw.set_ylabel('Magnitude (a. u.)', fontsize=14)

    fft_pix_data *= composite_filter

    if noise_threshold is not None:
        fft_pix_data[np.abs(fft_pix_data) < noise_floor] = 1E-16  # DON'T use 0 here. ipython kernel dies

    if show_plots:
        ax_filt.semilogy(w_vec[x_lims], np.abs(fft_pix_data)[x_lims])
        ax_filt.set_title('Filtered Signal', fontsize=16)
        ax_filt.set_xlabel('Frequency(kHz)', fontsize=14)
        ax_filt.set_ylabel('Magnitude (a. u.)', fontsize=14)
        if noise_threshold is not None:
            ax_filt.set_ylim(bottom=noise_floor)  # prevents the noise threshold from messing up plots
        fig_fft.tight_layout()

    filt_data = np.real(np.fft.ifft(np.fft.ifftshift(fft_pix_data)))

    if verbose:
        print('The shape of the filtered data is {}'.format(filt_data.shape))
        print('The shape of the excitation waveform is {}'.format(excit_wfm.shape))

    fig_loops = None
    if show_loops:
        if len(resp_wfm) == len(excit_wfm):
            # single plot:
            fig_loops, axis = plt.subplots(figsize=(5.5, 5))
            axis.plot(excit_wfm, resp_wfm, 'r', label='Raw')
            axis.plot(excit_wfm, filt_data, 'k', label='Filtered')
            axis.legend(fontsize=14)
            set_tick_font_size(axis, 14)
            axis.set_xlabel('Excitation', fontsize=16)
            axis.set_ylabel('Signal', fontsize=16)
            axis.set_title(plot_title, fontsize=16)
            fig_loops.tight_layout()
        else:
            # N loops:
            raw_pixels = np.reshape(resp_wfm, (-1, len(excit_wfm)))
            filt_pixels = np.reshape(filt_data, (-1, len(excit_wfm)))
            print(raw_pixels.shape, filt_pixels.shape)

            fig_loops, axes_loops = plot_curves(excit_wfm, [raw_pixels, filt_pixels], line_colors=['r', 'k'],
                                                dataset_names=['Raw', 'Filtered'], x_label='Excitation',
                                                y_label='Signal', subtitle_prefix='Col ', num_plots=16,
                                                title=plot_title)

    return filt_data, fig_fft, fig_loops