Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/Tools/FilterTestTool/FilterTest.py
Views: 1798
# -*- coding: utf-8 -*-12""" ArduPilot IMU Filter Test Class34This program is free software: you can redistribute it and/or modify it under5the terms of the GNU General Public License as published by the Free Software6Foundation, either version 3 of the License, or (at your option) any later7version.8This program is distributed in the hope that it will be useful, but WITHOUT9ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS10FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.11You should have received a copy of the GNU General Public License along with12this program. If not, see <http://www.gnu.org/licenses/>.13"""1415__author__ = "Guglielmo Cassinelli"16__contact__ = "[email protected]"1718import numpy as np19import matplotlib.pyplot as plt20from matplotlib.widgets import Slider21from matplotlib.animation import FuncAnimation22from scipy import signal23from BiquadFilter import BiquadFilterType, BiquadFilter2425sliders = [] # matplotlib sliders must be global26anim = None # matplotlib animations must be global272829class FilterTest:30FILTER_DEBOUNCE = 10 # ms3132FILT_SHAPE_DT_FACTOR = 1 # increase to reduce filter shape size3334FFT_N = 5123536filters = {}3738def __init__(self, acc_t, acc_x, acc_y, acc_z, gyr_t, gyr_x, gyr_y, gyr_z, acc_freq, gyr_freq,39acc_lpf_cutoff, gyr_lpf_cutoff,40acc_notch_freq, acc_notch_att, acc_notch_band,41gyr_notch_freq, gyr_notch_att, gyr_notch_band,42log_name, accel_notch=False, second_notch=False):4344self.filter_color_map = plt.get_cmap('summer')4546self.filters["acc"] = [47BiquadFilter(acc_lpf_cutoff, acc_freq)48]4950if accel_notch:51self.filters["acc"].append(52BiquadFilter(acc_notch_freq, acc_freq, BiquadFilterType.PEAK, acc_notch_att, acc_notch_band),53)5455self.filters["gyr"] = [56BiquadFilter(gyr_lpf_cutoff, gyr_freq),57BiquadFilter(gyr_notch_freq, gyr_freq, BiquadFilterType.PEAK, gyr_notch_att, gyr_notch_band)58]5960if second_notch:61self.filters["acc"].append(62BiquadFilter(acc_notch_freq * 2, acc_freq, BiquadFilterType.PEAK, acc_notch_att, acc_notch_band)63)64self.filters["gyr"].append(65BiquadFilter(gyr_notch_freq * 2, gyr_freq, BiquadFilterType.PEAK, gyr_notch_att, gyr_notch_band)66)6768self.ACC_t = acc_t69self.ACC_x = acc_x70self.ACC_y = acc_y71self.ACC_z = acc_z7273self.GYR_t = gyr_t74self.GYR_x = gyr_x75self.GYR_y = gyr_y76self.GYR_z = gyr_z7778self.GYR_freq = gyr_freq79self.ACC_freq = acc_freq8081self.gyr_dt = 1. / gyr_freq82self.acc_dt = 1. / acc_freq8384self.timer = None8586self.updated_artists = []8788# INIT89self.init_plot(log_name)9091def test_acc_filters(self):92filt_xs = self.test_filters(self.filters["acc"], self.ACC_t, self.ACC_x)93filt_ys = self.test_filters(self.filters["acc"], self.ACC_t, self.ACC_y)94filt_zs = self.test_filters(self.filters["acc"], self.ACC_t, self.ACC_z)95return filt_xs, filt_ys, filt_zs9697def test_gyr_filters(self):98filt_xs = self.test_filters(self.filters["gyr"], self.GYR_t, self.GYR_x)99filt_ys = self.test_filters(self.filters["gyr"], self.GYR_t, self.GYR_y)100filt_zs = self.test_filters(self.filters["gyr"], self.GYR_t, self.GYR_z)101return filt_xs, filt_ys, filt_zs102103def test_filters(self, filters, Ts, Xs):104for f in filters:105f.reset()106107x_filtered = []108109for i, t in enumerate(Ts):110x = Xs[i]111112x_f = x113for filt in filters:114x_f = filt.apply(x_f)115116x_filtered.append(x_f)117118return x_filtered119120def get_filter_shape(self, filter):121samples = int(filter.get_sample_freq()) # resolution of filter shape based on sample rate122x_space = np.linspace(0.0, samples // 2, samples // int(2 * self.FILT_SHAPE_DT_FACTOR))123return x_space, filter.freq_response(x_space)124125def init_signal_plot(self, ax, Ts, Xs, Ys, Zs, Xs_filtered, Ys_filtered, Zs_filtered, label):126ax.plot(Ts, Xs, linewidth=1, label="{}X".format(label), alpha=0.5)127ax.plot(Ts, Ys, linewidth=1, label="{}Y".format(label), alpha=0.5)128ax.plot(Ts, Zs, linewidth=1, label="{}Z".format(label), alpha=0.5)129filtered_x_ax, = ax.plot(Ts, Xs_filtered, linewidth=1, label="{}X filtered".format(label), alpha=1)130filtered_y_ax, = ax.plot(Ts, Ys_filtered, linewidth=1, label="{}Y filtered".format(label), alpha=1)131filtered_z_ax, = ax.plot(Ts, Zs_filtered, linewidth=1, label="{}Z filtered".format(label), alpha=1)132ax.legend(prop={'size': 8})133return filtered_x_ax, filtered_y_ax, filtered_z_ax134135def fft_to_xdata(self, fft):136n = len(fft)137norm_factor = 2. / n138return norm_factor * np.abs(fft[:n // 2])139140def plot_fft(self, ax, x, fft, label):141fft_ax, = ax.plot(x, self.fft_to_xdata(fft), label=label)142return fft_ax143144def init_fft(self, ax, Ts, Xs, Ys, Zs, sample_rate, dt, Xs_filtered, Ys_filtered, Zs_filtered, label):145146_freqs_raw_x, _times_raw_x, _stft_raw_x = signal.stft(Xs, sample_rate, window='hann', nperseg=self.FFT_N)147raw_fft_x = np.average(np.abs(_stft_raw_x), axis=1)148149_freqs_raw_y, _times_raw_y, _stft_raw_y = signal.stft(Ys, sample_rate, window='hann', nperseg=self.FFT_N)150raw_fft_y = np.average(np.abs(_stft_raw_y), axis=1)151152_freqs_raw_z, _times_raw_z, _stft_raw_z = signal.stft(Zs, sample_rate, window='hann', nperseg=self.FFT_N)153raw_fft_z = np.average(np.abs(_stft_raw_z), axis=1)154155_freqs_x, _times_x, _stft_x = signal.stft(Xs_filtered, sample_rate, window='hann', nperseg=self.FFT_N)156filtered_fft_x = np.average(np.abs(_stft_x), axis=1)157158_freqs_y, _times_y, _stft_y = signal.stft(Ys_filtered, sample_rate, window='hann', nperseg=self.FFT_N)159filtered_fft_y = np.average(np.abs(_stft_y), axis=1)160161_freqs_z, _times_z, _stft_z = signal.stft(Zs_filtered, sample_rate, window='hann', nperseg=self.FFT_N)162filtered_fft_z = np.average(np.abs(_stft_z), axis=1)163164ax.plot(_freqs_raw_x, raw_fft_x, alpha=0.5, linewidth=1, label="{}x FFT".format(label))165ax.plot(_freqs_raw_y, raw_fft_y, alpha=0.5, linewidth=1, label="{}y FFT".format(label))166ax.plot(_freqs_raw_z, raw_fft_z, alpha=0.5, linewidth=1, label="{}z FFT".format(label))167168filtered_fft_ax_x, = ax.plot(_freqs_x, filtered_fft_x, label="filt. {}x FFT".format(label))169filtered_fft_ax_y, = ax.plot(_freqs_y, filtered_fft_y, label="filt. {}y FFT".format(label))170filtered_fft_ax_z, = ax.plot(_freqs_z, filtered_fft_z, label="filt. {}z FFT".format(label))171172# FFT173# samples = len(Ts)174# x_space = np.linspace(0.0, 1.0 / (2.0 * dt), samples // 2)175# filtered_data = np.hanning(len(Xs_filtered)) * Xs_filtered176# raw_fft = np.fft.fft(np.hanning(len(Xs)) * Xs)177# filtered_fft = np.fft.fft(filtered_data, n=self.FFT_N)178# self.plot_fft(ax, x_space, raw_fft, "{} FFT".format(label))179# fft_freq = np.fft.fftfreq(self.FFT_N, d=dt)180# x_space181# filtered_fft_ax = self.plot_fft(ax, fft_freq[:self.FFT_N // 2], filtered_fft, "filtered {} FFT".format(label))182183ax.set_xlabel("frequency")184# ax.set_xscale("log")185# ax.xaxis.set_major_formatter(ScalarFormatter())186ax.legend(prop={'size': 8})187188return filtered_fft_ax_x, filtered_fft_ax_y, filtered_fft_ax_z189190def init_filter_shape(self, ax, filter, color):191center = filter.get_center_freq()192x_space, lpf_shape = self.get_filter_shape(filter)193194plot_slpf_shape, = ax.plot(x_space, lpf_shape, c=color, label="LPF shape")195xvline_lpf_cutoff = ax.axvline(x=center, linestyle="--", c=color) # LPF cutoff freq196197return plot_slpf_shape, xvline_lpf_cutoff198199def create_slider(self, name, rect, max, value, color, callback):200global sliders201ax_slider = self.fig.add_axes(rect, facecolor='lightgoldenrodyellow')202slider = Slider(ax_slider, name, 0, max, valinit=np.sqrt(max * value), valstep=1, color=color)203slider.valtext.set_text(value)204205# slider.drawon = False206207def changed(val, cbk, max, slider):208# non linear slider to better control small values209val = int(val ** 2 / max)210slider.valtext.set_text(val)211cbk(val)212213slider.on_changed(lambda val, cbk=callback, max=max, s=slider: changed(val, cbk, max, s))214sliders.append(slider)215216def delay_update(self, update_cbk):217def _delayed_update(self, cbk):218self.timer.stop()219cbk()220221# delay actual filtering222if self.fig:223if self.timer:224self.timer.stop()225self.timer = self.fig.canvas.new_timer(interval=self.FILTER_DEBOUNCE)226self.timer.add_callback(lambda self=self: _delayed_update(self, update_cbk))227self.timer.start()228229def update_filter_shape(self, filter, shape, center_line):230x_data, new_shape = self.get_filter_shape(filter)231232shape.set_ydata(new_shape)233center_line.set_xdata(filter.get_center_freq())234235self.updated_artists.extend([236shape,237center_line,238])239240def update_signal_and_fft_plot(self, filters_key, time_list, sample_lists, signal_shapes, fft_shapes, shape,241center_line, sample_rate):242# print("update_signal_and_fft_plot", self.filters[filters_key][0].get_center_freq())243Xs, Ys, Zs = sample_lists244signal_shape_x, signal_shape_y, signal_shape_z = signal_shapes245fft_shape_x, fft_shape_y, fft_shape_z = fft_shapes246247Xs_filtered = self.test_filters(self.filters[filters_key], time_list, Xs)248Ys_filtered = self.test_filters(self.filters[filters_key], time_list, Ys)249Zs_filtered = self.test_filters(self.filters[filters_key], time_list, Zs)250251signal_shape_x.set_ydata(Xs_filtered)252signal_shape_y.set_ydata(Ys_filtered)253signal_shape_z.set_ydata(Zs_filtered)254255self.updated_artists.extend([signal_shape_x, signal_shape_y, signal_shape_z])256257_freqs_x, _times_x, _stft_x = signal.stft(Xs_filtered, sample_rate, window='hann', nperseg=self.FFT_N)258filtered_fft_x = np.average(np.abs(_stft_x), axis=1)259260_freqs_y, _times_y, _stft_y = signal.stft(Ys_filtered, sample_rate, window='hann', nperseg=self.FFT_N)261filtered_fft_y = np.average(np.abs(_stft_y), axis=1)262263_freqs_z, _times_z, _stft_z = signal.stft(Zs_filtered, sample_rate, window='hann', nperseg=self.FFT_N)264filtered_fft_z = np.average(np.abs(_stft_z), axis=1)265266fft_shape_x.set_ydata(filtered_fft_x)267fft_shape_y.set_ydata(filtered_fft_y)268fft_shape_z.set_ydata(filtered_fft_z)269270self.updated_artists.extend([271fft_shape_x, fft_shape_y, fft_shape_z,272shape, center_line,273])274275# self.fig.canvas.draw()276277def animation_update(self):278updated_artists = self.updated_artists.copy()279280# if updated_artists:281# print("animation update")282283# reset updated artists284self.updated_artists = []285286return updated_artists287288def update_filter(self, val, cbk, filter, shape, center_line, filters_key, time_list, sample_lists, signal_shapes,289fft_shapes):290# this callback sets the parameter controlled by the slider291cbk(val)292# print("filter update",val)293# update filter shape and delay fft update294self.update_filter_shape(filter, shape, center_line)295sample_freq = filter.get_sample_freq()296self.delay_update(297lambda self=self: self.update_signal_and_fft_plot(filters_key, time_list, sample_lists, signal_shapes,298fft_shapes, shape, center_line, sample_freq))299300def create_filter_control(self, name, filter, rect, max, default, shape, center_line, cbk, filters_key, time_list,301sample_lists, signal_shapes, fft_shapes, filt_color):302self.create_slider(name, rect, max, default, filt_color, lambda val, cbk=cbk, self=self, filter=filter, shape=shape,303center_line=center_line, filters_key=filters_key,304time_list=time_list, sample_list=sample_lists,305signal_shape=signal_shapes, fft_shape=fft_shapes:306self.update_filter(val, cbk, filter, shape, center_line, filters_key,307time_list, sample_list, signal_shape, fft_shape))308309def create_controls(self, filters_key, base_rect, padding, ax_fft, time_list, sample_lists, signal_shapes,310fft_shapes):311ax_filter = ax_fft.twinx()312ax_filter.set_navigate(False)313ax_filter.set_yticks([])314315num_filters = len(self.filters[filters_key])316317for i, filter in enumerate(self.filters[filters_key]):318filt_type = filter.get_type()319filt_color = self.filter_color_map(i / num_filters)320filt_shape, filt_cutoff = self.init_filter_shape(ax_filter, filter, filt_color)321322if filt_type == BiquadFilterType.PEAK:323name = "Notch"324else:325name = "LPF"326327# control for center freq is common to all filters328self.create_filter_control("{} freq".format(name), filter, base_rect, 500, filter.get_center_freq(),329filt_shape, filt_cutoff,330lambda val, filter=filter: filter.set_center_freq(val),331filters_key, time_list, sample_lists, signal_shapes, fft_shapes, filt_color)332# move down of control height + padding333base_rect[1] -= (base_rect[3] + padding)334335if filt_type == BiquadFilterType.PEAK:336self.create_filter_control("{} att (db)".format(name), filter, base_rect, 100, filter.get_attenuation(),337filt_shape, filt_cutoff,338lambda val, filter=filter: filter.set_attenuation(val),339filters_key, time_list, sample_lists, signal_shapes, fft_shapes, filt_color)340base_rect[1] -= (base_rect[3] + padding)341self.create_filter_control("{} band".format(name), filter, base_rect, 300, filter.get_bandwidth(),342filt_shape, filt_cutoff,343lambda val, filter=filter: filter.set_bandwidth(val),344filters_key, time_list, sample_lists, signal_shapes, fft_shapes, filt_color)345base_rect[1] -= (base_rect[3] + padding)346347def create_spectrogram(self, data, name, sample_rate):348freqs, times, Sx = signal.spectrogram(np.array(data), fs=sample_rate, window='hanning',349nperseg=self.FFT_N, noverlap=self.FFT_N - self.FFT_N // 10,350detrend=False, scaling='spectrum')351352f, ax = plt.subplots(figsize=(4.8, 2.4))353ax.pcolormesh(times, freqs, 10 * np.log10(Sx), cmap='viridis')354ax.set_title(name)355ax.set_ylabel('Frequency (Hz)')356ax.set_xlabel('Time (s)')357358def init_plot(self, log_name):359360self.fig = plt.figure(figsize=(14, 9))361self.fig.canvas.set_window_title("ArduPilot Filter Test Tool - {}".format(log_name))362self.fig.canvas.draw()363364rows = 2365cols = 3366raw_acc_index = 1367fft_acc_index = raw_acc_index + 1368raw_gyr_index = cols + 1369fft_gyr_index = raw_gyr_index + 1370371# signal372self.ax_acc = self.fig.add_subplot(rows, cols, raw_acc_index)373self.ax_gyr = self.fig.add_subplot(rows, cols, raw_gyr_index, sharex=self.ax_acc)374375accx_filtered, accy_filtered, accz_filtered = self.test_acc_filters()376self.ax_filtered_accx, self.ax_filtered_accy, self.ax_filtered_accz = self.init_signal_plot(self.ax_acc,377self.ACC_t,378self.ACC_x,379self.ACC_y,380self.ACC_z,381accx_filtered,382accy_filtered,383accz_filtered,384"AccX")385386gyrx_filtered, gyry_filtered, gyrz_filtered = self.test_gyr_filters()387self.ax_filtered_gyrx, self.ax_filtered_gyry, self.ax_filtered_gyrz = self.init_signal_plot(self.ax_gyr,388self.GYR_t,389self.GYR_x,390self.GYR_y,391self.GYR_z,392gyrx_filtered,393gyry_filtered,394gyrz_filtered,395"GyrX")396397# FFT398self.ax_acc_fft = self.fig.add_subplot(rows, cols, fft_acc_index)399self.ax_gyr_fft = self.fig.add_subplot(rows, cols, fft_gyr_index)400401self.acc_filtered_fft_ax_x, self.acc_filtered_fft_ax_y, self.acc_filtered_fft_ax_z = self.init_fft(402self.ax_acc_fft, self.ACC_t, self.ACC_x, self.ACC_y, self.ACC_z, self.ACC_freq, self.acc_dt, accx_filtered,403accy_filtered, accz_filtered, "AccX")404self.gyr_filtered_fft_ax_x, self.gyr_filtered_fft_ax_y, self.gyr_filtered_fft_ax_z = self.init_fft(405self.ax_gyr_fft, self.GYR_t, self.GYR_x, self.GYR_y, self.GYR_z, self.GYR_freq, self.gyr_dt, gyrx_filtered,406gyry_filtered, gyrz_filtered, "GyrX")407408self.fig.tight_layout()409410# TODO add y z411self.create_controls("acc", [0.75, 0.95, 0.2, 0.02], 0.01, self.ax_acc_fft, self.ACC_t,412(self.ACC_x, self.ACC_y, self.ACC_z),413(self.ax_filtered_accx, self.ax_filtered_accy, self.ax_filtered_accz),414(self.acc_filtered_fft_ax_x, self.acc_filtered_fft_ax_y, self.acc_filtered_fft_ax_z))415self.create_controls("gyr", [0.75, 0.45, 0.2, 0.02], 0.01, self.ax_gyr_fft, self.GYR_t,416(self.GYR_x, self.GYR_y, self.GYR_z),417(self.ax_filtered_gyrx, self.ax_filtered_gyry, self.ax_filtered_gyrz),418(self.gyr_filtered_fft_ax_x, self.gyr_filtered_fft_ax_y, self.gyr_filtered_fft_ax_z))419420# setup animation for continuous update421global anim422anim = FuncAnimation(self.fig, lambda frame, self=self: self.animation_update(), interval=1, blit=False)423424# Work in progress here...425# self.create_spectrogram(self.GYR_x, "GyrX", self.GYR_freq)426# self.create_spectrogram(gyrx_filtered, "GyrX filtered", self.GYR_freq)427# self.create_spectrogram(self.ACC_x, "AccX", self.ACC_freq)428# self.create_spectrogram(accx_filtered, "AccX filtered", self.ACC_freq)429430plt.show()431432self.print_filter_param_info()433434def print_filter_param_info(self):435if len(self.filters["acc"]) > 2 or len(self.filters["gyr"]) > 2:436print("Testing too many filters unsupported from firmware, cannot calculate parameters to set them")437return438439print("To have the last filter settings in the graphs set the following parameters:\n")440441for f in self.filters["acc"]:442filt_type = f.get_type()443444if filt_type == BiquadFilterType.PEAK: # NOTCH445print("INS_NOTCA_ENABLE,", 1)446print("INS_NOTCA_FREQ,", f.get_center_freq())447print("INS_NOTCA_BW,", f.get_bandwidth())448print("INS_NOTCA_ATT,", f.get_attenuation())449else: # LPF450print("INS_ACCEL_FILTER,", f.get_center_freq())451452for f in self.filters["gyr"]:453filt_type = f.get_type()454455if filt_type == BiquadFilterType.PEAK: # NOTCH456print("INS_HNTC2_ENABLE,", 1)457print("INS_HNTC2_FREQ,", f.get_center_freq())458print("INS_HNTC2_BW,", f.get_bandwidth())459print("INS_HNTC2_ATT,", f.get_attenuation())460else: # LPF461print("INS_GYRO_FILTER,", f.get_center_freq())462463print("\n+---------+")464print("| WARNING |")465print("+---------+")466print("Always check the onboard FFT to setup filters, this tool only simulate effects of filtering.")467468469