Skip to content

Commit

Permalink
Merge pull request #108 from card-io-ecg/filt
Browse files Browse the repository at this point in the history
Design filters in compile-time
  • Loading branch information
bugadani committed Aug 31, 2023
2 parents 2969589 + d82b1e7 commit 73aa0cb
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 21 deletions.
3 changes: 3 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ quote = "1.0.9"
darling = "0.20.1"
proc-macro2 = "1.0.29"

# for designfilt
sci-rs = "0.2.7"

[lib]
proc-macro = true
144 changes: 144 additions & 0 deletions macros/src/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use sci_rs::signal::filter::design::{
iirfilter_dyn, BaFormatFilter, DigitalFilter, FilterBandType, FilterType,
};
use std::collections::HashMap;
use syn::{
parse::{Parse, ParseBuffer},
Lit, LitStr, Token,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FilterKind {
HighPassIir,
}

impl Parse for FilterKind {
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
let kind = input.parse::<LitStr>()?;

let filter_kind = kind.value().to_ascii_lowercase();
let filter_kind = match filter_kind.as_str() {
"highpassiir" => FilterKind::HighPassIir,

_ => {
return Err(syn::Error::new(
kind.span(),
format!("unknown filter type: {filter_kind}"),
));
}
};

Ok(filter_kind)
}
}

pub struct FilterSpec {
filter_kind: FilterKind,
span: Span,
options: HashMap<String, f32>,
}

impl Parse for FilterSpec {
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
let filter_kind = input.parse::<FilterKind>()?;
let mut options = HashMap::new();

while !input.is_empty() {
input.parse::<Token![,]>()?;
let key = input.parse::<LitStr>()?;

let keystr = validate_filter_option(filter_kind, &key)?;

input.parse::<Token![,]>()?;
let value = input.parse::<Lit>()?;

let value = match value {
Lit::Int(lit) => lit.base10_parse().unwrap(),
Lit::Float(lit) => lit.base10_parse().unwrap(),
_ => return Err(syn::Error::new_spanned(value, "expected a number")),
};

if options.insert(keystr, value).is_some() {
return Err(syn::Error::new(
key.span(),
format!("duplicate option: {}", key.value()),
));
}
}

Ok(Self {
filter_kind,
span: input.span(),
options,
})
}
}

fn validate_filter_option(filter_kind: FilterKind, key: &LitStr) -> syn::Result<String> {
let expected = match filter_kind {
FilterKind::HighPassIir => &[
"filterorder",
"passbandfrequency",
"halfpowerfrequency",
"passbandripple",
"samplerate",
],
};

let value = key.value().to_ascii_lowercase();
if !expected.contains(&value.as_str()) {
return Err(syn::Error::new(
key.span(),
format!("'{value}' is not expected for '{filter_kind:?}'"),
));
}

Ok(value)
}

pub fn run(args: FilterSpec) -> TokenStream {
let module = quote! {crate::filter};

match args.filter_kind {
FilterKind::HighPassIir => {
let Some(&order) = args.options.get("filterorder") else {
return syn::Error::new(args.span, "missing required option 'FilterOrder'")
.to_compile_error();
};

let filter = iirfilter_dyn(
order as usize,
vec![args.options.get("halfpowerfrequency").copied().unwrap()],
None,
None,
Some(FilterBandType::Highpass),
Some(FilterType::Butterworth),
Some(false),
None,
args.options.get("samplerate").copied(),
);

let DigitalFilter::Ba(BaFormatFilter { mut b, mut a }) = filter else {
unreachable!()
};

let zeros_a = a.iter().rev().take_while(|&&x| x == 0.0).count();
let zeros_b = b.iter().rev().take_while(|&&x| x == 0.0).count();

let remove = zeros_a.min(zeros_b);

a.truncate(a.len() - remove);
b.truncate(b.len() - remove);

assert!(a.swap_remove(0) == 1.0);

a.reverse();

quote! {
#module::iir::Iir::new(&[#(#b,)*], &[#(#a,)*])
}
}
}
}
7 changes: 7 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use syn::{
Token,
};

mod filter;
mod task;

struct Args {
Expand Down Expand Up @@ -61,3 +62,9 @@ pub fn task(args: TokenStream, item: TokenStream) -> TokenStream {

task::run(&args.meta, f).into()
}

#[proc_macro]
pub fn designfilt(item: TokenStream) -> TokenStream {
let spec = syn::parse_macro_input!(item as filter::FilterSpec);
filter::run(spec).into()
}
1 change: 1 addition & 0 deletions signal-processing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ object-chain = { workspace = true }
micromath = { version = "2.0.0", optional = true }
num-complex = { version = "0.4.3", default-features = false }
qrs_detector = { git = "https://github.com/bugadani/QrsDetector.git" }
macros = { path = "../macros" }

[features]
nostd = ["micromath"]
36 changes: 18 additions & 18 deletions signal-processing/src/filter/iir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@ use core::marker::PhantomData;
pub mod precomputed {
use super::{HighPass, Iir};

/// designfilt('highpassiir', 'FilterOrder', 2, 'HalfPowerFrequency', 50, 'SampleRate', 1000)
pub const HIGH_PASS_50HZ: Iir<'static, HighPass, 2> = Iir::new(
&[0.800_592_4, -1.601_184_8, 0.800_592_4],
&[-1.561_018_1, 0.641_351_5],
#[rustfmt::skip]
pub const HIGH_PASS_FOR_DISPLAY: Iir<'static, HighPass, 2> = macros::designfilt!(
"highpassiir",
"FilterOrder", 2,
"HalfPowerFrequency", 0.5,
"SampleRate", 1000
);

/// designfilt('highpassiir', 'FilterOrder', 2, 'HalfPowerFrequency', 80, 'SampleRate', 1000)
pub const HIGH_PASS_80HZ: Iir<'static, HighPass, 2> = Iir::new(
&[0.699_774_3, -1.399_548_6, 0.699_774_3],
&[-1.307_285_1, 0.491_812_23],
#[rustfmt::skip]
pub const HIGH_PASS_50HZ: Iir<'static, HighPass, 2> = macros::designfilt!(
"highpassiir",
"FilterOrder", 2,
"HalfPowerFrequency", 50,
"SampleRate", 1000
);

/// designfilt('highpassiir', 'FilterOrder', 2, 'PassbandFrequency', 1.59, 'PassbandRipple', 1, 'SampleRate', 1000)
pub const HIGH_PASS_CUTOFF_1_59HZ: Iir<'static, HighPass, 2> = Iir::new(
&[0.886_820_26, -1.773_640_5, 0.886_820_26],
&[-1.990_012_3, 0.990_102_35],
);

/// designfilt('highpassiir', 'FilterOrder', 2, 'PassbandFrequency', .55, 'PassbandRipple', 1, 'SampleRate', 50)
pub const HIGH_PASS_CUTOFF_0_55HZ: Iir<'static, HighPass, 2> = Iir::new(
&[0.860_691_6, -1.721_383_2, 0.860_691_6],
&[-1.929_33, 0.933_517_46],
#[rustfmt::skip]
pub const HIGH_PASS_80HZ: Iir<'static, HighPass, 2> = macros::designfilt!(
"highpassiir",
"FilterOrder", 2,
"HalfPowerFrequency", 80,
"SampleRate", 1000
);
}

Expand Down
5 changes: 2 additions & 3 deletions src/states/measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use signal_processing::{
compressing_buffer::CompressingBuffer,
filter::{
downsample::DownSampler,
iir::{precomputed::HIGH_PASS_CUTOFF_1_59HZ, HighPass, Iir},
iir::{precomputed::HIGH_PASS_FOR_DISPLAY, HighPass, Iir},
pli::{adaptation_blocking::AdaptationBlocking, PowerLineFilter},
Filter,
},
Expand Down Expand Up @@ -85,8 +85,7 @@ impl EcgObjects {
#[inline(always)]
fn new() -> Self {
Self {
filter: Chain::new(HIGH_PASS_CUTOFF_1_59HZ)
.append(PowerLineFilter::new(1000.0, [50.0])),
filter: Chain::new(HIGH_PASS_FOR_DISPLAY).append(PowerLineFilter::new(1000.0, [50.0])),
downsampler: Chain::new(DownSampler::new())
.append(DownSampler::new())
.append(DownSampler::new()),
Expand Down

0 comments on commit 73aa0cb

Please sign in to comment.