Skip to main content

cardio_rs/analysis/
windows_analysis.rs

1//! A module providing functionality for performing window-based analysis of Heart Rate
2//! Variability (HRV) metrics over non-overlapping windows of RR intervals.
3//!
4//! The analysis is done over fixed-duration windows, where each window is processed
5//! independently, and the results for each window are stored. The user can customize the
6//! pipeline for HRV metric calculation by implementing the `AnalysisPipeline` trait, or
7//! they can use the default implementation that computes various HRV metrics.
8//!
9//! ## Key Components:
10//!
11//! - `WindowsAnalysisBuilder<T>`: A builder that allows you to configure and generate a
12//!   `WindowsAnalysis` with user-defined data and pipeline.
13//! - `AnalysisPipeline<T>`: A trait that allows the user to define custom pipelines to
14//!   process RR interval data and compute various HRV metrics.
15//! - `DefaultPipeline`: A default implementation of the `AnalysisPipeline` trait that computes
16//!   HRV metrics using the `TimeMetrics`, `FrequencyMetrics`, and `GeometricMetrics` modules.
17//! - `WindowsAnalysis<T>`: A struct representing the result of the windowed HRV analysis,
18//!   containing the computed HRV metrics for each window.
19//!
20//! ## Example Usage:
21//!
22//! ### Basic Example (Using Default Pipeline):
23//! ```rust
24//! use cardio_rs::utils::test_data::RR_INTERVALS;
25//! use cardio_rs::windows_analysis::WindowsAnalysisBuilder;
26//!
27//! let rr_intervals = RR_INTERVALS.to_vec();
28//! let window_size = 60_000.0; // 1-minute window size
29//!
30//! let windows_analysis = WindowsAnalysisBuilder::new(rr_intervals)
31//!     .with_window_size(window_size)
32//!     .build();
33//!
34//! for (i, hrv_metrics) in windows_analysis.metrics.iter().enumerate() {
35//!     println!("Window {}: HRV Metrics: {:?}", i, hrv_metrics);
36//! }
37//! ```
38//!
39//! ### Custom Pipeline Example:
40//! You can define your own custom analysis pipeline by implementing the `AnalysisPipeline` trait.
41//! This allows you to customize how HRV metrics are computed over the RR intervals.
42//!
43//! ```rust
44//! use cardio_rs::utils::test_data::RR_INTERVALS;
45//! use cardio_rs::{windows_analysis::{WindowsAnalysisBuilder}, HrvMetrics, geometric_domain::GeometricMetrics, non_linear::NonLinearMetrics, processing_utils::{EctopicMethod, RRIntervals, DetectOutliers, AnalysisPipeline}, time_domain::TimeMetrics, frequency_domain::FrequencyMetrics};
46//!
47//! // Define a custom pipeline by implementing the AnalysisPipeline trait
48//! struct CustomPipeline;
49//!
50//! impl AnalysisPipeline<f64> for CustomPipeline
51//! {
52//!     fn process(&self, data: Vec<f64>) -> HrvMetrics<f64> {
53//!       let mut rr_intervals = RRIntervals::new(data);
54//!       rr_intervals.detect_ectopics(EctopicMethod::Karlsson);
55//!       rr_intervals.detect_outliers(&300., &2_000.);
56//!       rr_intervals.remove_outliers_ectopics();
57//!
58//!       let time = TimeMetrics::compute(rr_intervals.as_slice());
59//!       let frequency = FrequencyMetrics::compute(rr_intervals.as_slice(), 10.);
60//!       let geometric = GeometricMetrics::compute(rr_intervals.as_slice());
61//!       let non_linear = NonLinearMetrics::compute_default(rr_intervals.as_slice());
62//!
63//!       HrvMetrics {
64//!           time,
65//!           frequency,
66//!           geometric,
67//!           non_linear,
68//!       }
69//!     }
70//! }
71//!
72//! let rr_intervals = RR_INTERVALS.to_vec();
73//!
74//! // Create the window analysis with the custom pipeline and non-overlapping windows
75//! let custom_pipeline = Box::new(CustomPipeline);
76//! let windows_analysis = WindowsAnalysisBuilder::new(rr_intervals)
77//!     .with_window_size(60_000.0) // Set the window size (1 minute)
78//!     .with_pipeline(custom_pipeline) // Use the custom pipeline
79//!     .build();
80//!
81//! // Print HRV metrics for each window
82//! for (i, hrv_metrics) in windows_analysis.metrics.iter().enumerate() {
83//!     println!("Window {}: HRV Metrics: {:?}", i, hrv_metrics);
84//! }
85//! ```
86
87#[cfg(not(feature = "std"))]
88extern crate alloc;
89use crate::{HrvMetrics, processing_utils::AnalysisPipeline};
90#[cfg(not(feature = "std"))]
91use alloc::{boxed::Box, vec::Vec};
92use core::iter::Sum;
93use num::Float;
94
95/// A struct that holds the HRV metrics for each window in the analysis.
96///
97/// `WindowsAnalysis<T>` stores the HRV metrics computed for each window of RR intervals.
98/// The `metrics` vector contains the HRV results for all the non-overlapping windows
99/// that have been processed. Each window is defined by the specified `window_size`.
100pub struct WindowsAnalysis<T> {
101    /// The size of the sliding window used for the analysis.
102    pub window_size: T,
103
104    /// The list of HRV metrics computed for each window.
105    pub metrics: Vec<HrvMetrics<T>>,
106}
107
108/// A builder struct for configuring and constructing a `WindowsAnalysis`.
109///
110/// The `WindowsAnalysisBuilder` struct is used to configure the sliding window size, the analysis pipeline,
111/// and the data for HRV computation. After the builder is configured, the `build()` method is used to generate
112/// a `WindowsAnalysis` instance containing HRV metrics for each window.
113pub struct WindowsAnalysisBuilder<T> {
114    /// The RR intervals data used for the analysis.
115    data: Vec<T>,
116
117    /// The pipeline used to process the RR intervals and compute HRV metrics.
118    pipeline: Box<dyn AnalysisPipeline<T>>,
119
120    /// The size of the window used for the analysis.
121    window_size: T,
122}
123
124impl<
125    T: Float
126        + Sum<T>
127        + Copy
128        + core::fmt::Debug
129        + num::Signed
130        + 'static
131        + core::ops::AddAssign
132        + core::marker::Send
133        + core::marker::Sync
134        + num::FromPrimitive,
135> WindowsAnalysisBuilder<T>
136where
137    Box<dyn AnalysisPipeline<T>>: Default,
138{
139    /// Creates a new `WindowsAnalysisBuilder` with the provided data.
140    ///
141    /// # Arguments
142    /// * `data` - A vector of RR intervals to be analyzed.
143    ///
144    /// # Returns
145    /// Returns a new `WindowsAnalysisBuilder` instance.
146    pub fn new(data: Vec<T>) -> Self {
147        Self {
148            data,
149            window_size: T::from(60_000).unwrap(),
150            pipeline: Default::default(),
151        }
152    }
153
154    /// Sets the window size for the analysis.
155    ///
156    /// # Arguments
157    /// * `window_size` - The size of each window, in milliseconds.
158    ///
159    /// # Returns
160    /// Returns the builder instance for chaining.
161    pub fn with_window_size(mut self, window_size: T) -> Self {
162        self.window_size = window_size;
163        self
164    }
165
166    /// Sets the pipeline used to process the RR intervals and compute HRV metrics.
167    ///
168    /// # Arguments
169    /// * `pipeline` - The pipeline used to process the data.
170    ///
171    /// # Returns
172    /// Returns the builder instance for chaining.
173    pub fn with_pipeline(mut self, pipeline: Box<dyn AnalysisPipeline<T>>) -> Self {
174        self.pipeline = pipeline;
175        self
176    }
177
178    /// Builds the `WindowsAnalysis` struct based on the current configuration of the builder.
179    ///
180    /// This method processes the data into windows, computes the HRV metrics for each window,
181    /// and stores the results in a `WindowsAnalysis` instance.
182    ///
183    /// # Returns
184    /// Returns a `WindowsAnalysis` containing the HRV metrics for each window.
185    pub fn build(&self) -> WindowsAnalysis<T> {
186        let metrics = self
187            .data
188            .iter()
189            .enumerate()
190            .scan((T::from(0).unwrap(), 0), |s, (index, &i)| {
191                s.0 += i;
192                if index == self.data.len() - 1 {
193                    Some(Some((s.1, self.data.len())))
194                } else if s.0 <= self.window_size {
195                    Some(None)
196                } else {
197                    s.0 = T::from(0).unwrap();
198                    let prev = s.1;
199                    s.1 = index;
200                    Some(Some((prev, s.1)))
201                }
202            })
203            .flatten()
204            .map(|i| self.pipeline.process(self.data[i.0..i.1].to_vec()))
205            .collect();
206
207        WindowsAnalysis {
208            metrics,
209            window_size: self.window_size,
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::utils::test_data::RR_INTERVALS;
218
219    #[test]
220    fn test_window_size() {
221        let data = RR_INTERVALS.to_vec();
222        let hrv = WindowsAnalysisBuilder::new(data.clone()).build();
223        assert_eq!(hrv.metrics.len(), 5);
224
225        let hrv = WindowsAnalysisBuilder::new(data.clone())
226            .with_window_size(30_000.)
227            .build();
228        assert_eq!(hrv.metrics.len(), 10);
229    }
230
231    #[test]
232    fn test_windows_analysis() {
233        let mut data = RR_INTERVALS.to_vec();
234        data.extend_from_within(..);
235        let hrv = WindowsAnalysisBuilder::new(data.clone())
236            .with_window_size(597_000. / 2.)
237            .build();
238        assert_eq!(hrv.metrics[0], hrv.metrics[1]);
239    }
240}