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}