cardio_rs/utils/
processing_utils.rs

1//! A module providing functionality for detecting and handling outliers and ectopic beats in a series of RR intervals.
2//!
3//! It defines the `RRIntervals` struct which contains a vector of RR intervals, and optional fields to store indices of
4//! detected outliers and ectopic beats. The module also provides a trait `DetectOutliers` which can be implemented to detect
5//! outliers and ectopic beats using custom methods, while default implementations for detecting outliers and ectopic beats
6//! using common algorithms are also provided. The module includes methods for removing detected outliers and ectopics from
7//! the RR intervals data.
8//!
9//! ## Key Components:
10//! - `RRIntervals`: The main struct representing a collection of RR intervals and optional outlier/ectopic detections.
11//! - `DetectOutliers`: A trait for detecting outliers and ectopics with customizable implementations.
12//! - `EctopicMethod`: An enum that defines different methods for detecting ectopic beats in RR intervals.
13//!
14//! ## Features:
15//! - **Detecting Outliers**: Outliers in the RR intervals are detected by comparing each interval to a provided range (`lowest_rr`, `highest_rr`).
16//! - **Detecting Ectopics**: Ectopic beats are detected using methods like the Karlsson method, which compares the mean of adjacent intervals.
17//! - **Flexible Customization**: Users can implement their own methods for detecting outliers and ectopics by implementing the `DetectOutliers` trait.
18//! - **Removing Outliers and Ectopics**: Detected outliers and ectopics can be removed from the data using the `remove_outliers_ectopics` method, leaving only the valid RR intervals.
19//!
20//! ## Example Usage:
21//!
22//! ```rust
23//! use cardio_rs::processing_utils::{RRIntervals, EctopicMethod, DetectOutliers};
24//!
25//! let mut rr_intervals = RRIntervals::new(vec![800.0, 850.0, 3000.0, 600.0, 800.0]);
26//! rr_intervals.detect_outliers(&300.0, &2000.0);  // Detect outliers based on specified range
27//! rr_intervals.detect_ectopics(EctopicMethod::Karlsson);  // Detect ectopic beats using the Karlsson method
28//! rr_intervals.remove_outliers_ectopics();  // Remove outliers and ectopics from the RR intervals data
29//! ```
30//!
31//! ## Trait and Struct Documentation:
32//! - `RRIntervals<T>`: A struct representing a sequence of RR intervals along with optional detected outliers and ectopics.
33//! - `DetectOutliers<T>`: A trait for detecting outliers and ectopics. Custom implementations can be provided by the user.
34//! - `EctopicMethod`: An enum for specifying different methods to detect ectopic beats (currently only `Karlsson` is supported).
35//!
36
37#[cfg(not(feature = "std"))]
38extern crate alloc;
39#[cfg(not(feature = "std"))]
40use alloc::{boxed::Box, vec, vec::Vec};
41use core::{
42    iter::Sum,
43    ops::{Deref, DerefMut},
44};
45use num::Float;
46
47/// Enum representing different methods for detecting ectopic beats in RR intervals.
48///
49/// This enum provides various algorithms for identifying and removing ectopic beats
50/// based on predefined statistical criteria.
51pub enum EctopicMethod {
52    /// Karlsson method for detecting ectopic beats in RR intervals.
53    ///
54    /// An RR interval is considered ectopic if it differs by more than 20%
55    /// from the mean of the previous and next RR intervals.
56    Karlsson,
57
58    /// Acar method for detecting ectopic beats in RR intervals.
59    ///
60    /// An RR interval is considered ectopic if it differs by more than 20%
61    /// from the mean of the last 9 RR intervals.
62    Acar,
63}
64
65/// Struct representing RR intervals and associated outlier and ectopic detection results.
66///
67/// This struct contains a vector of RR intervals and optional fields to store indices of
68/// detected outliers and ectopic beats. It provides methods to detect these events
69/// and remove them from the data as needed.
70#[derive(Debug)]
71pub struct RRIntervals<T> {
72    /// A vector of RR intervals representing the data.
73    rr_intervals: Vec<T>,
74
75    /// An optional vector storing indices of detected outliers in the RR intervals.
76    outliers: Option<Vec<usize>>,
77
78    /// An optional vector storing indices of detected ectopic beats in the RR intervals.
79    ectopics: Option<Vec<usize>>,
80}
81
82/// Trait for detecting outliers and ectopics in RR intervals.
83///
84/// This trait allows custom implementations of methods for detecting outliers and ectopic beats in RR intervals.
85/// Any type that implements this trait can provide different detection methods based on specific requirements.
86pub trait DetectOutliers<T> {
87    fn detect_outliers(&mut self, lowest_rr: &T, highest_rr: &T);
88    fn detect_ectopics(&mut self, method: EctopicMethod);
89}
90
91impl<T: Float + Sum<T> + Copy + core::fmt::Debug + num::Signed + 'static + num::FromPrimitive>
92    DetectOutliers<T> for RRIntervals<T>
93{
94    /// Detects outliers in the RR intervals based on the given `lowest_rr` and `highest_rr`.
95    ///
96    /// The method identifies RR intervals that are either lower than `lowest_rr` or higher than `highest_rr`
97    /// as outliers. These outliers are stored in the `outliers` field of the struct.
98    ///
99    /// # Arguments
100    ///
101    /// * `lowest_rr` - The minimum acceptable RR interval.
102    /// * `highest_rr` - The maximum acceptable RR interval.
103    ///
104    /// # Example
105    ///
106    /// ```rust
107    /// use cardio_rs::processing_utils::{RRIntervals, DetectOutliers};
108    ///
109    /// let mut rr_intervals = RRIntervals::new(vec![800.0, 850.0, 3000.0, 600.0, 800.0]);
110    /// rr_intervals.detect_outliers(&300.0, &2000.0);
111    /// ```
112    fn detect_outliers(&mut self, lowest_rr: &T, highest_rr: &T) {
113        let outliers: Vec<usize> = self
114            .iter()
115            .enumerate()
116            .filter_map(|(index, value)| {
117                if lowest_rr > value || value > highest_rr {
118                    Some(index)
119                } else {
120                    None
121                }
122            })
123            .collect();
124        self.outliers = if outliers.is_empty() {
125            None
126        } else {
127            Some(outliers)
128        };
129    }
130
131    /// Detects ectopic beats in the RR intervals using a specified method.
132    ///
133    /// This method detects ectopic beats based on the given `method` (e.g., the `Karlsson` method),
134    /// and stores the detected indices of ectopics in the `ectopics` field of the struct.
135    ///
136    /// # Arguments
137    ///
138    /// * `method` - The method used to detect ectopic beats.
139    ///
140    /// # Example
141    ///
142    /// ```rust
143    /// use cardio_rs::processing_utils::{RRIntervals, EctopicMethod, DetectOutliers};
144    ///
145    /// let mut rr_intervals = RRIntervals::new(vec![800.0, 850.0, 900.0, 600.0, 800.0]);
146    /// rr_intervals.detect_ectopics(EctopicMethod::Karlsson);
147    /// ```
148    fn detect_ectopics(&mut self, method: EctopicMethod) {
149        let ectopics: Vec<usize> = match method {
150            EctopicMethod::Karlsson => (0..self.len() - 2)
151                .filter_map(|i| {
152                    let mean = (self[i] + self[i + 2]) / T::from(2).unwrap();
153                    if (mean - self[i + 1]).abs() >= T::from(0.2).unwrap() * mean {
154                        Some(i + 1)
155                    } else {
156                        None
157                    }
158                })
159                .collect(),
160            EctopicMethod::Acar => (9..self.len())
161                .filter(|&i| {
162                    let mean = (self[i - 9..i].iter().cloned().sum::<T>()) / T::from(9).unwrap();
163                    (mean - self[i]).abs() >= T::from(0.2).unwrap() * mean
164                })
165                .collect(),
166        };
167        self.ectopics = if ectopics.is_empty() {
168            None
169        } else {
170            Some(ectopics)
171        };
172    }
173}
174
175impl<T> Deref for RRIntervals<T> {
176    type Target = Vec<T>;
177
178    /// Deref implementation for accessing the underlying `Vec<T>` of RR intervals.
179    ///
180    /// This implementation allows easy read-only access to the vector of RR intervals.
181    fn deref(&self) -> &Self::Target {
182        &self.rr_intervals
183    }
184}
185
186impl<T> DerefMut for RRIntervals<T> {
187    /// DerefMut implementation for mutable access to the underlying `Vec<T>` of RR intervals.
188    ///
189    /// This implementation allows modifying the vector of RR intervals directly.
190    fn deref_mut(&mut self) -> &mut Self::Target {
191        &mut self.rr_intervals
192    }
193}
194
195impl<T: Float + Sum<T> + Copy + core::fmt::Debug + num::Signed + 'static + num::FromPrimitive>
196    RRIntervals<T>
197{
198    /// Creates a new instance of `RRIntervals` from a vector of RR intervals.
199    ///
200    /// # Arguments
201    ///
202    /// * `rr_intervals` - A vector of RR intervals.
203    ///
204    /// # Returns
205    ///
206    /// A new `RRIntervals` instance containing the provided RR intervals, with `None` values for outliers and ectopics.
207    pub fn new(rr_intervals: Vec<T>) -> Self {
208        Self {
209            rr_intervals,
210            outliers: None,
211            ectopics: None,
212        }
213    }
214
215    /// Removes outliers and ectopics from the RR intervals.
216    ///
217    /// This method removes any elements from the RR intervals vector at indices that
218    /// correspond to detected outliers or ectopics. After removal, the `outliers` and `ectopics`
219    /// fields are reset to `None`.
220    pub fn remove_outliers_ectopics(&mut self) {
221        self.rr_intervals = self
222            .iter()
223            .enumerate()
224            .filter_map(|(i, j)| {
225                if self.outliers.as_ref().unwrap_or(&vec![]).contains(&i)
226                    || self.ectopics.as_ref().unwrap_or(&vec![]).contains(&i)
227                {
228                    None
229                } else {
230                    Some(*j)
231                }
232            })
233            .collect::<Vec<T>>();
234        self.ectopics = None;
235        self.outliers = None;
236    }
237}
238
239/// A trait for processing HRV data through an analysis pipeline.
240pub trait AnalysisPipeline<T> {
241    /// Processes a vector of RR intervals to compute HRV metrics.
242    ///
243    /// # Arguments
244    /// * `data` - A vector of RR intervals representing the time between heartbeats for a given window.
245    ///
246    /// # Returns
247    /// Returns a struct containing the computed HRV metrics (time-domain, frequency-domain, and geometric).
248    fn process(&self, data: Vec<T>) -> crate::HrvMetrics<T>;
249}
250
251/// A default implementation of the `AnalysisPipeline` trait that computes HRV metrics using a predefined method.
252struct DefaultPipeline();
253impl<
254    T: Float
255        + Sum<T>
256        + Copy
257        + core::fmt::Debug
258        + num::Signed
259        + 'static
260        + core::ops::AddAssign
261        + core::marker::Send
262        + core::marker::Sync
263        + Into<f64>
264        + num::FromPrimitive,
265> AnalysisPipeline<T> for DefaultPipeline
266{
267    fn process(&self, data: Vec<T>) -> crate::HrvMetrics<T> {
268        let mut rr_intervals = RRIntervals::new(data);
269        rr_intervals.detect_ectopics(EctopicMethod::Karlsson);
270        rr_intervals.detect_outliers(&T::from(300).unwrap(), &T::from(2_000).unwrap());
271        rr_intervals.remove_outliers_ectopics();
272
273        let time = crate::time_domain::TimeMetrics::compute(rr_intervals.as_slice());
274        let frequency = crate::frequency_domain::FrequencyMetrics::compute(
275            rr_intervals.as_slice(),
276            T::from(4).unwrap(),
277        );
278        let geometric = crate::geometric_domain::GeometricMetrics::compute(rr_intervals.as_slice());
279        let non_linear =
280            crate::non_linear::NonLinearMetrics::compute_default(rr_intervals.as_slice());
281
282        crate::HrvMetrics {
283            time,
284            frequency,
285            geometric,
286            non_linear,
287        }
288    }
289}
290
291impl<
292    T: Float
293        + Sum<T>
294        + Copy
295        + core::fmt::Debug
296        + num::Signed
297        + 'static
298        + core::ops::AddAssign
299        + core::marker::Send
300        + core::marker::Sync
301        + num::FromPrimitive,
302> Default for Box<dyn AnalysisPipeline<T>>
303where
304    f64: From<T>,
305{
306    /// Returns a default instance of the `DefaultPipeline`.
307    ///
308    /// This method returns the default `DefaultPipeline` that computes HRV metrics using default methods.
309    fn default() -> Self {
310        Box::new(DefaultPipeline())
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::utils::test_data::RR_INTERVALS;
318
319    #[test]
320    fn test_remove_none() {
321        let mut rr_intervals = RRIntervals::new(RR_INTERVALS.to_vec());
322        rr_intervals.remove_outliers_ectopics();
323        assert_eq!(RR_INTERVALS, *rr_intervals);
324    }
325
326    #[test]
327    fn test_remove_ectopics_karlsson() {
328        let mut rr_intervals = RRIntervals::new(vec![800., 850., 900., 600., 800., 820., 840.]);
329        rr_intervals.detect_ectopics(EctopicMethod::Karlsson);
330        assert_eq!(Some(vec![2, 3]), rr_intervals.ectopics);
331        rr_intervals.remove_outliers_ectopics();
332        assert_eq!(vec![800., 850., 800., 820., 840.], *rr_intervals);
333    }
334
335    #[test]
336    fn test_remove_ectopics_acar() {
337        let mut rr_intervals = RRIntervals::new(vec![
338            800., 850., 3000., 600., 800., 820., 240., 800., 850., 3000., 600., 800., 820., 240.,
339        ]);
340        rr_intervals.detect_ectopics(EctopicMethod::Acar);
341        assert_eq!(Some(vec![9, 10, 11, 13]), rr_intervals.ectopics);
342        rr_intervals.remove_outliers_ectopics();
343        assert_eq!(
344            vec![800., 850., 3000., 600., 800., 820., 240., 800., 850., 820.],
345            *rr_intervals
346        );
347    }
348
349    #[test]
350    fn test_remove_outliers() {
351        let mut rr_intervals = RRIntervals::new(vec![800., 850., 3000., 600., 800., 820., 240.]);
352        rr_intervals.detect_outliers(&300., &2_000.);
353        assert_eq!(Some(vec![2, 6]), rr_intervals.outliers);
354        rr_intervals.remove_outliers_ectopics();
355        assert_eq!(vec![800., 850., 600., 800., 820.], *rr_intervals);
356    }
357
358    #[test]
359    fn test_remove_combined() {
360        let mut rr_intervals = RRIntervals::new(vec![800., 850., 3000., 600., 800., 820., 240.]);
361        rr_intervals.outliers = Some(vec![2, 3, 4]);
362        rr_intervals.ectopics = Some(vec![4, 5]);
363        rr_intervals.remove_outliers_ectopics();
364        assert_eq!(vec![800., 850., 240.], *rr_intervals);
365    }
366}