From 2c240c9298b363087409953ca0402d75788e4ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 10 Oct 2023 10:58:46 +0200 Subject: [PATCH] refactor --- src/bin/main.rs | 112 ++++++++++++++++++++++----------- src/bin/stream_test.rs | 14 +++-- src/psd.rs | 138 +++++++++++++++++++++++++++++------------ 3 files changed, 185 insertions(+), 79 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 246abba..13dda55 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -3,7 +3,8 @@ use anyhow::Result; use clap::Parser; use eframe::egui::plot::{Legend, Line, Plot, PlotPoints}; -use eframe::egui::{self, ComboBox, Slider}; +use eframe::egui::{self, ComboBox, ProgressBar, Slider}; +use stabilizer_streaming::{AvgOpts, MergeOpts}; use std::sync::mpsc; use std::time::Duration; @@ -37,19 +38,19 @@ pub struct Opts { pub struct AcqOpts { /// Exclude PSD stages with less than or equal this averaging level #[arg(short, long, default_value_t = 1)] - min_avg: usize, + min_avg: u32, /// Segment detrending method - #[arg(short, long, default_value = "mid")] + #[arg(short, long, default_value = "midpoint")] detrend: Detrend, /// Sample rate in Hertz #[arg(short, long, default_value_t = 1.0f32)] fs: f32, - /// Exponential averaging - #[arg(short, long, default_value_t = 100000)] - max_avg: usize, + /// Boxcar/Exponential averaging count + #[arg(short, long, default_value_t = 1000)] + max_avg: u32, /// Enable for constant time averaging across stages /// Disable for constant count averaging across stages @@ -78,7 +79,10 @@ fn main() -> Result<()> { let mut dec = PsdCascade::<{ 1 << 9 }>::default(); dec.set_stage_depth(3); dec.set_detrend(acq.detrend); - dec.set_avg(acq.scale_avg, acq.max_avg); + dec.set_avg(AvgOpts { + scale: acq.scale_avg, + count: acq.max_avg, + }); dec })); } @@ -98,16 +102,23 @@ fn main() -> Result<()> { Err(mpsc::TryRecvError::Empty) => {} Ok(Cmd::Send(opts)) => { acq = opts; + let merge_opts = MergeOpts { + remove_overlap: acq.min_avg > 0, + min_count: acq.min_avg, + }; for dec in dec.iter_mut() { dec.set_detrend(acq.detrend); - dec.set_avg(acq.scale_avg, acq.max_avg); + dec.set_avg(AvgOpts { + scale: acq.scale_avg, + count: acq.max_avg, + }); } let logfs = acq.fs.log10(); let trace = dec .iter() .map(|dec| { - let (p, b) = dec.psd(acq.min_avg); - let f = Break::frequencies(&b, acq.min_avg > 0); + let (p, b) = dec.psd(&merge_opts); + let f = Break::frequencies(&b, &merge_opts); let (mut p0, mut f0) = (0.0, 0.0); Trace { breaks: b, @@ -156,11 +167,11 @@ fn main() -> Result<()> { ..Default::default() }; eframe::run_native( - "FLS", + "PSD", options, Box::new(move |_cc| { // cc.egui_ctx.set_visuals(egui::Visuals::light()); - Box::new(FLS::new(trace_recv, cmd_send, acq)) + Box::new(App::new(trace_recv, cmd_send, acq)) }), ) .unwrap(); @@ -170,7 +181,7 @@ fn main() -> Result<()> { Ok(()) } -pub struct FLS { +pub struct App { trace_recv: mpsc::Receiver>, cmd_send: mpsc::Sender, current: Vec, @@ -178,7 +189,7 @@ pub struct FLS { repaint: f32, } -impl FLS { +impl App { fn new( trace_recv: mpsc::Receiver>, cmd_send: mpsc::Sender, @@ -194,35 +205,36 @@ impl FLS { } } -impl eframe::App for FLS { +impl eframe::App for App { fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { self.cmd_send.send(Cmd::Exit).unwrap(); } fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - match self.trace_recv.try_recv() { - Err(mpsc::TryRecvError::Empty) => {} - Ok(new) => { - self.current = new; - ctx.request_repaint_after(Duration::from_secs_f32(self.repaint)) - } - Err(mpsc::TryRecvError::Disconnected) => panic!("lost data processing thread"), - }; + match self.trace_recv.try_recv() { + Err(mpsc::TryRecvError::Empty) => {} + Ok(new) => { + self.current = new; + ctx.request_repaint_after(Duration::from_secs_f32(self.repaint)) + } + Err(mpsc::TryRecvError::Disconnected) => panic!("lost data processing thread"), + }; + egui::CentralPanel::default().show(ctx, |ui| { ui.horizontal(|ui| { ui.add( Slider::new(&mut self.repaint, 0.01..=10.0) .text("Repaint") .suffix(" s") .logarithmic(true), - ).on_hover_text("Request repaint after timeout (seconds)"); + ) + .on_hover_text("Request repaint after timeout (seconds)"); ui.separator(); ComboBox::from_label("Detrend") .selected_text(format!("{:?}", self.acq.detrend)) .show_ui(ui, |ui| { ui.selectable_value(&mut self.acq.detrend, Detrend::None, "None"); - ui.selectable_value(&mut self.acq.detrend, Detrend::Mid, "Mid"); + ui.selectable_value(&mut self.acq.detrend, Detrend::Midpoint, "Midpoint"); ui.selectable_value(&mut self.acq.detrend, Detrend::Span, "Span"); ui.selectable_value(&mut self.acq.detrend, Detrend::Mean, "Mean"); // ui.selectable_value(&mut self.acq.detrend, Detrend::Linear, "Linear"); @@ -234,15 +246,20 @@ impl eframe::App for FLS { Slider::new(&mut self.acq.max_avg, 1..=1_000_000) .text("Averages") .logarithmic(true), - ).on_hover_text("Averaging count: when below, the averaging is boxcar, when above it continues with exponential averaging"); + ) + .on_hover_text( + "Averaging count: the averaging starts as boxcar, then continues exponential", + ); ui.separator(); - ui.checkbox(&mut self.acq.scale_avg, "Constant time avg").on_hover_text("Scale stage averaging count by stage dependent sample rate"); + ui.checkbox(&mut self.acq.scale_avg, "Scale averages") + .on_hover_text("Scale stage averaging count by stage dependent sample rate"); ui.separator(); ui.add( Slider::new(&mut self.acq.min_avg, 0..=self.acq.max_avg) - .text("Min Count") + .text("Min averages") .logarithmic(true), - ).on_hover_text("Minimum averaging count to show data from a stage"); + ) + .on_hover_text("Minimum averaging count to show data from a stage"); }); ui.horizontal(|ui| { ui.add( @@ -254,12 +271,34 @@ impl eframe::App for FLS { ) .on_hover_text("Input sample rate"); ui.separator(); - ui.checkbox(&mut self.acq.integrate, "Integrate").on_hover_text("Integrate PSD into cumulative sum"); - ui.separator(); - self.current - .first() - .and_then(|t| t.breaks.first()) - .map(|bi| ui.label(format!("{:.2e}", bi.count as f32)).on_hover_text("Top level average count")); + ui.checkbox(&mut self.acq.integrate, "Integrate") + .on_hover_text("Integrate PSD into linear cumulative sum"); + if let Some(t) = self.current.first() { + if let Some(bi) = t.breaks.first() { + ui.separator(); + ui.add( + ProgressBar::new(bi.count as f32 / bi.avg as f32) + .desired_width(50.0) + .show_percentage(), + ) + .on_hover_text("Top averaging fill"); + } + if let Some(bi) = t.breaks.last() { + ui.separator(); + ui.label(format!( + "{:.2e}", + bi.processed as f32 * (1u64 << bi.decimation) as f32 + )) + .on_hover_text("Bottom effective number of input samples processed"); + ui.separator(); + ui.add( + ProgressBar::new(bi.pending as f32 / bi.fft_size as f32) + .desired_width(50.0) + .show_percentage(), + ) + .on_hover_text("Bottom buffer fill (incl overlap)"); + } + } ui.separator(); if ui .button("Reset") @@ -284,6 +323,7 @@ impl eframe::App for FLS { } }); }); + self.cmd_send.send(Cmd::Send(self.acq)).unwrap(); } } diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 2a9d24c..53bb5f3 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -2,7 +2,7 @@ use anyhow::Result; use clap::Parser; use stabilizer_streaming::{ source::{Source, SourceOpts}, - Break, Detrend, PsdCascade, VarBuilder, + Break, Detrend, MergeOpts, PsdCascade, VarBuilder, }; use std::sync::mpsc; use std::time::Duration; @@ -28,6 +28,10 @@ fn main() -> Result<()> { duration, trace, } = Opts::parse(); + let merge_opts = MergeOpts { + remove_overlap: true, + min_count: 1, + }; let (cmd_send, cmd_recv) = mpsc::channel(); let receiver = std::thread::spawn(move || { @@ -37,7 +41,7 @@ fn main() -> Result<()> { .map(|_| { let mut c = PsdCascade::<{ 1 << 9 }>::default(); c.set_stage_depth(3); - c.set_detrend(Detrend::Mid); + c.set_detrend(Detrend::Midpoint); c }) .collect(); @@ -53,7 +57,7 @@ fn main() -> Result<()> { }; } - let (y, b) = dec[trace].psd(1); + let (y, b) = dec[trace].psd(&merge_opts); log::info!("breaks: {:?}", b); log::info!("psd: {:?}", y); @@ -61,8 +65,8 @@ fn main() -> Result<()> { let var = VarBuilder::default().dc_cut(1).clip(1.0).build().unwrap(); let mut fdev = vec![]; let mut tau = 1.0; - let f = Break::frequencies(&b, true); - while tau <= (b0.effective_fft_size / 2) as f32 { + let f = Break::frequencies(&b, &merge_opts); + while tau <= (b0.effective_fft_size() / 2) as f32 { fdev.push((tau, var.eval(&y, &f, tau).sqrt())); tau *= 2.0; } diff --git a/src/psd.rs b/src/psd.rs index 2868eac..6b4cc35 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -61,7 +61,7 @@ pub enum Detrend { #[default] None, /// Subtract the midpoint of each segment - Mid, + Midpoint, /// Remove linear interpolation between first and last item for each segment Span, /// Remove the mean of the segment @@ -88,7 +88,7 @@ impl Detrend { c.im = 0.0; } } - Detrend::Mid => { + Detrend::Midpoint => { let offset = x[N / 2]; for ((c, x), w) in c.iter_mut().zip(x.iter()).zip(win.win.iter()) { c.re = (x - offset) * w; @@ -126,12 +126,12 @@ pub struct Psd { buf: [f32; N], idx: usize, spectrum: [f32; N], // using only the positive half N/2 + 1 - count: usize, + count: u32, drain: usize, fft: Arc>, win: Arc>, detrend: Detrend, - avg: usize, + avg: u32, } impl Psd { @@ -150,13 +150,13 @@ impl Psd { win, detrend: Detrend::default(), drain: 0, - avg: usize::MAX, + avg: u32::MAX, }; s.set_stage_depth(0); s } - pub fn set_avg(&mut self, avg: usize) { + pub fn set_avg(&mut self, avg: u32) { self.avg = avg; } @@ -166,7 +166,7 @@ impl Psd { pub fn set_stage_depth(&mut self, n: usize) { self.hbf.set_depth(n); - self.drain = self.hbf.response_length(); + self.drain = self.hbf.response_length() as _; } } @@ -197,7 +197,7 @@ pub trait PsdStage { /// one-sided fn gain(&self) -> f32; /// Number of averages - fn count(&self) -> usize; + fn count(&self) -> u32; /// Currently buffered input items fn buf(&self) -> &[f32]; } @@ -271,14 +271,14 @@ impl PsdStage for Psd { &self.spectrum[..N / 2 + 1] } - fn count(&self) -> usize { + fn count(&self) -> u32 { self.count } fn gain(&self) -> f32 { // 2 for one-sided // overlap is compensated by counting - (N / 2 * self.count) as f32 * self.win.nenbw * self.win.power + (N as u32 / 2 * self.count) as f32 * self.win.nenbw * self.win.power } fn buf(&self) -> &[f32] { @@ -292,25 +292,31 @@ pub struct Break { /// Start index in PSD and frequencies pub start: usize, /// Number of averages - pub count: usize, + pub count: u32, + /// Averaging limit + pub avg: u32, /// Highes FFT bin (at `start`) pub highest_bin: usize, - /// The effective FFT size - pub effective_fft_size: usize, - /// Unprocessed number of input samples (excluding overlap) + /// FFT size + pub fft_size: usize, + /// The decimation power of two + pub decimation: usize, + /// Unprocessed number of input samples (includes overlap) pub pending: usize, + /// Total number of samples processed (excluding overlap, ignoring averaging) + pub processed: usize, } impl Break { /// Compute PSD bin center frequencies from stage breaks. - pub fn frequencies(b: &[Self], remove_overlap: bool) -> Vec { + pub fn frequencies(b: &[Self], opts: &MergeOpts) -> Vec { let Some(bi) = b.last() else { return vec![] }; let mut f = Vec::with_capacity(bi.start + bi.highest_bin); for bi in b.iter() { - if remove_overlap { + if opts.remove_overlap { f.truncate(bi.start); } - let df = 1.0 / bi.effective_fft_size as f32; + let df = 1.0 / bi.effective_fft_size() as f32; f.extend((0..bi.highest_bin).rev().map(|f| f as f32 * df)); } assert_eq!(f.len(), bi.start + bi.highest_bin); @@ -318,6 +324,46 @@ impl Break { debug_assert_eq!(f.last(), Some(&0.0)); f } + + pub fn effective_fft_size(&self) -> usize { + self.fft_size << self.decimation + } +} + +/// PSD segment merge options +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct MergeOpts { + /// Remove low resolution bins + pub remove_overlap: bool, + /// Minimum averaging level + pub min_count: u32, +} + +impl Default for MergeOpts { + fn default() -> Self { + Self { + remove_overlap: true, + min_count: 1, + } + } +} + +/// Averaging options +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct AvgOpts { + /// Scale averaging with decimation + pub scale: bool, + /// Averaging + pub count: u32, +} + +impl Default for AvgOpts { + fn default() -> Self { + Self { + scale: false, + count: u32::MAX, + } + } } /// Online power spectral density estimation @@ -348,7 +394,7 @@ pub struct PsdCascade { stage_depth: usize, detrend: Detrend, win: Arc>, - avg: (bool, usize), + avg: AvgOpts, } impl Default for PsdCascade { @@ -366,7 +412,7 @@ impl Default for PsdCascade { stage_depth: 1, detrend: Detrend::None, win, - avg: (false, usize::MAX), + avg: AvgOpts::default(), } } } @@ -384,10 +430,17 @@ impl PsdCascade { } } - pub fn set_avg(&mut self, scale: bool, avg: usize) { - self.avg = (scale, avg); + pub fn set_avg(&mut self, avg: AvgOpts) { + self.avg = avg; for (i, stage) in self.stages.iter_mut().enumerate() { - stage.set_avg((self.avg.1 >> if self.avg.0 { self.stage_depth * i } else { 0 }) as _); + stage.set_avg( + self.avg.count + >> if self.avg.scale { + self.stage_depth * i + } else { + 0 + }, + ); } } @@ -403,7 +456,14 @@ impl PsdCascade { let mut stage = Psd::new(self.fft.clone(), self.win.clone()); stage.set_stage_depth(self.stage_depth); stage.set_detrend(self.detrend); - stage.set_avg((self.avg.1 >> if self.avg.0 { self.stage_depth * i } else { 0 }) as _); + stage.set_avg( + self.avg.count + >> if self.avg.scale { + self.stage_depth * i + } else { + 0 + }, + ); self.stages.push(stage); } &mut self.stages[i] @@ -433,11 +493,11 @@ impl PsdCascade { /// # Returns /// * `psd`: `Vec` normalized reversed (Nyquist first, DC last) /// * `breaks`: `Vec` of stage breaks - pub fn psd(&self, min_count: usize) -> (Vec, Vec) { + pub fn psd(&self, opts: &MergeOpts) -> (Vec, Vec) { let mut p = Vec::with_capacity(self.stages.len() * (N / 2 + 1)); let mut b = Vec::with_capacity(self.stages.len()); let mut n = 0; - for stage in self.stages.iter().take_while(|s| s.count >= min_count) { + for stage in self.stages.iter().take_while(|s| s.count >= opts.min_count) { let mut pi = stage.spectrum(); // a stage yields frequency bins 0..N/2 from DC up to its nyquist // 0..floor(0.4*N) is its passband if it was preceeded by a decimator @@ -448,7 +508,7 @@ impl PsdCascade { // remove transition band of previous stage's decimator, floor let f_pass = 2 * N / 5; pi = &pi[..f_pass]; - if min_count > 0 { + if opts.remove_overlap { // remove low f bins from previous stage, ceil let d = stage.hbf.depth(); let f_low = (f_pass + (1 << d) - 1) >> d; @@ -458,14 +518,13 @@ impl PsdCascade { b.push(Break { start: p.len(), count: stage.count(), + avg: stage.avg, highest_bin: pi.len(), - effective_fft_size: N << n, - pending: stage.buf().len() - - if stage.count() > 0 { - stage.win.overlap - } else { - 0 - }, + fft_size: N, + decimation: n, + processed: ((N - stage.win.overlap) * stage.count() as usize + + stage.win.overlap * stage.count().min(1) as usize), + pending: stage.buf().len(), }); let g = (1 << n) as f32 / stage.gain(); p.extend(pi.iter().rev().map(|pi| pi * g)); @@ -504,7 +563,7 @@ mod test { fn insn() { let mut s = PsdCascade::<{ 1 << 9 }>::default(); s.set_stage_depth(3); - s.set_detrend(Detrend::Mid); + s.set_detrend(Detrend::Midpoint); let x: Vec<_> = (0..1 << 16) .map(|_| rand::random::() * 2.0 - 1.0) .collect(); @@ -512,7 +571,6 @@ mod test { // + 293 s.process(&x); } - println!("{:?}", s.psd(0).1.iter().take_while(|b| b.count > 0).last()); } /// full accuracy tests @@ -532,8 +590,12 @@ mod test { let mut s = PsdCascade::::default(); s.set_window(Window::hann()); s.process(&x); - let (p, b) = s.psd(0); - let f = Break::frequencies(&b, false); + let merge_opts = MergeOpts { + remove_overlap: false, + min_count: 0, + }; + let (p, b) = s.psd(&merge_opts); + let f = Break::frequencies(&b, &merge_opts); println!("{:?}, {:?}", p, f); assert!(p .iter() @@ -588,7 +650,7 @@ mod test { d.set_stage_depth(n); d.set_detrend(Detrend::None); d.process(&x); - let (p, b) = d.psd(1); + let (p, b) = d.psd(&MergeOpts::default()); // do not tweak DC and Nyquist! let n = p.len(); for (i, bi) in b.iter().enumerate() {