Filter: Examples: Add Transfer function check and MATLAB

This commit is contained in:
Iampete1 2023-05-20 13:05:25 +01:00 committed by Andrew Tridgell
parent 4406570391
commit f77635a5a6
3 changed files with 414 additions and 0 deletions

View File

@ -0,0 +1,206 @@
close all
clear
clc
% File generated by the TransferFunctionCheck example
filename = "test.csv";
% There are some anoying warnings for text parsing and matrix solve
warning('off','MATLAB:rankDeficientMatrix');
warning('off','MATLAB:table:ModifiedAndSavedVarnames');
data = readtable(filename);
lines = readlines(filename);
data_start = find(startsWith(lines,"f(hz),"));
if numel(data_start) ~= 1
error("Could not parse type")
end
lines = lines(1:data_start-1);
fprintf("%s\n",lines)
% Try and work out types
filters = {};
filter_index = find(startsWith(lines,"LowPassFilterFloat"));
for i = 1:numel(filter_index)
filters{end+1} = parse_low_pass(lines, filter_index(i)); %#ok<SAGROW>
end
filter_index = find(startsWith(lines,"LowPassFilter2pFloat"));
for i = 1:numel(filter_index)
filters{end+1} = parse_biquad(lines, filter_index(i)); %#ok<SAGROW>
end
filter_index = find(startsWith(lines,"NotchFilterFloat"));
for i = 1:numel(filter_index)
filters{end+1} = parse_notch(lines, filter_index(i)); %#ok<SAGROW>
end
if numel(filters) == 0
error("Failed to parse filters")
end
% Make sure all have the same sample rate
for i = 2:numel(filters)
if filters{i}.sample_rate ~= filters{1}.sample_rate
error("Sample rate mismatch")
end
end
sample_freq = filters{1}.sample_rate;
% Parse time from headers
for i = numel(data.Properties.VariableDescriptions):-1:2
time(i-1) = sscanf(data.Properties.VariableDescriptions{i},'t = %f');
end
if any(isnan(time))
error("Got NAN time")
end
% Parse input frequency and output amplitude
freq = data{:,1};
if any(isnan(freq))
error("Got NAN freq")
end
output = data{:,2:end};
if any(isnan(output))
error("Got NAN time")
end
% Brute force the amplitude and phase from the sweep
% This method is currently used in the js filter tool
% Best fit to sin to get amplitude, phase and DC offset
% https://math.stackexchange.com/questions/3926007/least-squares-regression-of-sine-wave
% Expecting output to be of same frequency at input
% Z = a*sin(t*kt + p) + O
% A = a*cos(p)
% B = a*sin(p)
% S = sin(t*kt)
% C = cos(t*kt)
num_sweep = numel(freq);
amplitude = zeros(num_sweep,1);
phase = zeros(num_sweep,1);
% Only fit to last 100 points, better aproximation for steady state
num_best_fit_points = 100;
best_fit_index = false(1,numel(time));
best_fit_index(end-num_best_fit_points+1:end) = true;
for i = 1:num_sweep
X = [sin(time(best_fit_index) * 2*pi*freq(i));
cos(time(best_fit_index) * 2*pi*freq(i));
zeros(1,num_best_fit_points)];
Y = output(i,best_fit_index);
% Z = a*sin(t*kt + p) + O
Z = Y / X;
amplitude(i) = sqrt(Z(1)^2 + Z(2)^2);
phase(i) = atan2d(Z(2), Z(1));
% DC_offset = Z(3);
end
% Caculalte using transfer function
Z = exp(1i*pi*(freq/(sample_freq*0.5)));
for i = numel(filters):-1:1
% Transfer function of each filter
H(:,i) = filters{i}.transfer_function(Z);
end
% multiply all transfer functions together
H_all = prod(H,2);
% Extract anmpitude and phase from transfer function
calc_amplitude = abs(H_all);
calc_phase = atan2d(imag(H_all),real(H_all));
% Caculate and print error size
amp_error = abs(amplitude - calc_amplitude);
fprintf('Amplitude error - max: %g mean: %g\n',max(amp_error), mean(amp_error))
phgase_error = abs(phase - calc_phase);
fprintf('Phase error - max: %g mean: %g\n',max(phgase_error), mean(phgase_error))
% Bode plot
figure
tiledlayout(2,1)
nexttile
hold all
plot(freq, amplitude)
plot(freq, calc_amplitude)
ylabel('magnitude')
legend('Brute force','transfer function')
nexttile
hold all
plot(freq, phase)
plot(freq, calc_phase)
xlabel('Frequency (Hz)')
ylabel('phase')
function ret = parse_low_pass(lines, start_index)
if ~startsWith(lines(start_index),"LowPassFilterFloat")
error("Expecting LowPassFilterFloat")
end
% Next line gives sample rate and cutoff
temp = sscanf(lines(start_index+1),'Sample rate: %f Hz, Cutoff: %f Hz');
ret.sample_rate = temp(1);
ret.target_freq = temp(2);
% Assume we know the form
alpha = sscanf(lines(start_index+3),'a: %f');
ret.transfer_function = @(z) alpha./(1-(1-alpha)*z.^-1);
end
function ret = parse_biquad(lines, start_index)
if ~startsWith(lines(start_index),"LowPassFilter2pFloat")
error("Expecting LowPassFilter2pFloat")
end
% Next line gives sample rate and cutoff
temp = sscanf(lines(start_index+1),'Sample rate: %f Hz, Cutoff: %f Hz');
ret.sample_rate = temp(1);
ret.target_freq = temp(2);
% Assume we know the form
coefficents = sscanf(lines(start_index+3),'a1: %f, a2: %f, b0: %f, b1: %f, b2: %f');
a1 = coefficents(1);
a2 = coefficents(2);
b0 = coefficents(3);
b1 = coefficents(4);
b2 = coefficents(5);
ret.transfer_function = @(z) (b0 + b1*z.^-1 + b2*z.^-2)./(1 + a1*z.^-1 + a2*z.^-2);
end
function ret = parse_notch(lines, start_index)
if ~startsWith(lines(start_index),"NotchFilterFloat")
error("Expecting NotchFilterFloat")
end
% Next line gives sample rate and cutoff
temp = sscanf(lines(start_index+1),'Sample rate: %f Hz, Center: %f Hz');
ret.sample_rate = temp(1);
ret.target_freq = temp(2);
% Assume we know the form
coefficents = sscanf(lines(start_index+3),'a0: %f, a1: %f, a2: %f, b0: %f, b1: %f, b2: %f');
a0 = coefficents(1);
a1 = coefficents(2);
a2 = coefficents(3);
b0 = coefficents(4);
b1 = coefficents(5);
b2 = coefficents(6);
ret.transfer_function = @(z) (b0 + b1*z.^-1 + b2*z.^-2)./(a0 + a1*z.^-1 + a2*z.^-2);
end

View File

@ -0,0 +1,201 @@
// Example and MATLAB script for verifying transfer functions of filters as implemented
/* on Linux run with
./waf configure --board linux
./waf --targets examples/TransferFunctionCheck
./build/linux/examples/TransferFunctionCheck
*/
#include <AP_HAL/AP_HAL.h>
#include <AP_Math/AP_Math.h>
#include <Filter/Filter.h>
#include <Filter/LowPassFilter2p.h>
#include <Filter/NotchFilter.h>
const AP_HAL::HAL& hal = AP_HAL::get_HAL();
// Some helper classes to allow accessing protected variables, also useful for adding type specific prints
class LowPassHelper : public LowPassFilterFloat {
public:
using LowPassFilterFloat::LowPassFilterFloat;
void set_cutoff_frequency_override(float sample_freq, float cutoff_freq) {
// Stash the sample rate so we can use it later
_sample_freq = sample_freq;
set_cutoff_frequency(sample_freq, cutoff_freq);
}
// Were really cheating here and using the same method as the filter to get the coefficient
// rather than pulling the coefficient directly
void print_transfer_function() {
hal.console->printf("LowPassFilterFloat\n");
hal.console->printf("Sample rate: %.9f Hz, Cutoff: %.9f Hz\n", _sample_freq, get_cutoff_freq());
hal.console->printf("Low pass filter in the form: H(z) = a/(1-(1-a)*z^-1)\n");
hal.console->printf("a: %.9f\n", calc_lowpass_alpha_dt(1.0/_sample_freq, get_cutoff_freq()));
}
private:
float _sample_freq;
};
class LowPass2pHelper : public LowPassFilter2pFloat {
public:
using LowPassFilter2pFloat::LowPassFilter2pFloat;
// Print transfer function and variables to console
void print_transfer_function() {
hal.console->printf("LowPassFilter2pFloat\n");
hal.console->printf("Sample rate: %.9f Hz, Cutoff: %.9f Hz\n", get_sample_freq(), get_cutoff_freq());
hal.console->printf("Biquad filter in the form: H(z) = (b0 + b1*z^-1 + b2*z^-2)/(1 + a1*z^-1 + a2*z^-2)\n");
hal.console->printf("a1: %.9f, a2: %.9f, b0: %.9f, b1: %.9f, b2: %.9f\n", _params.a1, _params.a2, _params.b0, _params.b1, _params.b2);
}
};
class NotchHelper : public NotchFilterFloat {
public:
using NotchFilterFloat::NotchFilterFloat;
// Print transfer function and variables to console
void print_transfer_function() {
hal.console->printf("NotchFilterFloat\n");
hal.console->printf("Sample rate: %.9f Hz, Center: %.9f Hz\n", _sample_freq_hz, _center_freq_hz);
hal.console->printf("Notch filter in the form: H(z) = (b0 + b1*z^-1 + b2*z^-2)/(a0 + a1*z^-1 + a2*z^-2)\n");
hal.console->printf("a0: %.9f, a1: %.9f, a2: %.9f, b0: %.9f, b1: %.9f, b2: %.9f\n", 1.0/a0_inv, a1, a2, b0, b1, b2);
}
};
// create an instance each filter to test
LowPassHelper lowpass;
LowPass2pHelper biquad;
NotchHelper notch;
NotchHelper notch2;
enum class filter_type {
LowPass,
Biquad,
Notch,
Combination,
} type;
void setup();
void loop();
void reset_all();
float apply_to_filter_under_test(float input);
void sweep(uint16_t num_samples, uint16_t max_freq, float sample_rate);
void loop() {};
// setup routine
void setup()
{
hal.console->printf("Frequency sweep transfer function test\n");
hal.console->printf("Sweeping a range of frequencies in the form sin(2*pi*t*f)\n");
// Set sample rate and target frequency
const float sample_rate = 1000;
const float target_freq = 50;
type = filter_type::Combination;
// Run 1000 time steps at each frequency
const uint16_t num_samples = 1000;
// Run upto 150 hz
const uint16_t max_freq = 150;
// Print transfer function of filter under test
hal.console->printf("\n");
switch (type) {
case filter_type::LowPass:
lowpass.set_cutoff_frequency_override(sample_rate, target_freq);
lowpass.print_transfer_function();
break;
case filter_type::Biquad:
biquad.set_cutoff_frequency(sample_rate, target_freq);
biquad.print_transfer_function();
break;
case filter_type::Notch:
notch.init(sample_rate, target_freq, target_freq*0.25, 40);
notch.print_transfer_function();
break;
case filter_type::Combination:
// A combination of two notches and a biquad lowpass, representative of a typical setup
notch.init(sample_rate, target_freq, target_freq*0.25, 40);
notch2.init(sample_rate, target_freq*2.00, target_freq*0.25, 40);
biquad.set_cutoff_frequency(sample_rate, target_freq*1.5);
notch.print_transfer_function();
hal.console->printf("\n");
notch2.print_transfer_function();
hal.console->printf("\n");
biquad.print_transfer_function();
break;
}
hal.console->printf("\n");
// Run sweep over given range
sweep(num_samples, max_freq, sample_rate);
// Wait a while for print buffer to empty and exit
hal.scheduler->delay(1000);
exit(0);
}
void reset_all()
{
lowpass.reset(0.0);
biquad.reset(0.0);
notch.reset();
notch2.reset();
}
float apply_to_filter_under_test(float input)
{
switch (type) {
case filter_type::LowPass:
return lowpass.apply(input);
case filter_type::Biquad:
return biquad.apply(input);
case filter_type::Notch:
return notch.apply(input);
case filter_type::Combination: // Ordering does not matter
return biquad.apply(notch.apply(notch2.apply(input)));
}
return input;
}
void sweep(uint16_t num_samples, uint16_t max_freq, float sample_rate)
{
// print header
hal.console->printf("f(hz)");
for (uint16_t i = 0; i < num_samples; i++) {
hal.console->printf(", t = %.9f", i / sample_rate);
}
hal.console->printf("\n");
for (uint16_t f = 1; f <= max_freq; f++) {
// Print freq and reset filter
hal.console->printf("%i", f);
reset_all();
// Run over the given number of samples
for (uint16_t i = 0; i < num_samples; i++) {
const float t = i / sample_rate;
const float input = sinf(M_2PI*t*f);
const float output = apply_to_filter_under_test(input);
hal.console->printf(", %+.9f", output);
}
hal.console->printf("\n");
}
}
AP_HAL_MAIN();

View File

@ -0,0 +1,7 @@
#!/usr/bin/env python
# encoding: utf-8
def build(bld):
bld.ap_example(
use='ap',
)