Skip to main content

cardio_rs/analysis/
live_analysis.rs

1//! A module providing the `TimeQueue` struct, which manages a fixed size time series of RR intervals
2//! and allows for real-time processing of heart rate variability (HRV) metrics. It utilizes
3//! a sliding window of RR intervals and processes them using a user-defined or default
4//! pipeline for HRV calculation.
5//!
6//! # Examples
7//!
8//! Basic usage with the default pipeline:
9//! ```
10//! use cardio_rs::utils::test_data::RR_INTERVALS;
11//! use cardio_rs::live_analysis::TimeQueue;
12//! let mut queue = TimeQueue::new(60_000);
13//! let rr_intervals = RR_INTERVALS.to_vec();
14//! for interval in rr_intervals {
15//!     queue.push(interval);
16//! }
17//! let hrv = queue.get_hrv();
18//! println!("Calculated HRV: {:?}", hrv);
19//! ```
20//!
21//! Custom pipeline usage:
22//! ```
23//! use cardio_rs::{live_analysis::TimeQueue, processing_utils::AnalysisPipeline, HrvMetrics};
24//! struct CustomPipeline;
25//! impl AnalysisPipeline<f64> for CustomPipeline {
26//!     fn process(&self, data: Vec<f64>) -> HrvMetrics<f64> {
27//!         HrvMetrics::default()
28//!     }
29//! }
30//! use cardio_rs::utils::test_data::RR_INTERVALS;
31//! let mut queue = TimeQueue::new(60_000);
32//! queue.set_pipeline(Box::new(CustomPipeline));
33//! let rr_intervals = RR_INTERVALS.to_vec();
34//! for interval in rr_intervals {
35//!     queue.push(interval);
36//! }
37//! let hrv = queue.get_hrv();
38//! println!("Calculated HRV with custom pipeline: {:?}", hrv);
39//! ```
40use crate::processing_utils::AnalysisPipeline;
41use num::Float;
42#[cfg(feature = "std")]
43use std::collections::VecDeque;
44#[cfg(not(feature = "std"))]
45extern crate alloc;
46#[cfg(not(feature = "std"))]
47use alloc::{boxed::Box, collections::vec_deque::VecDeque};
48
49/// A struct to manage a sliding window of RR intervals and process heart rate variability (HRV) metrics.
50/// It stores a collection of RR intervals in `data` and calculates HRV using a custom or default pipeline.
51pub struct TimeQueue<T> {
52    /// A `VecDeque` that stores the current RR intervals in the sliding window.
53    data: VecDeque<T>,
54
55    /// The total window time (in ms). Once this time is exceeded, the oldest RR interval is removed.
56    time: T,
57
58    /// The accumulated time that tracks how much time (in ms) has passed in the current window.
59    current_time: T,
60
61    /// The pipeline used to process the RR intervals and compute HRV metrics. It can be set to a custom pipeline.
62    pipeline: Box<dyn AnalysisPipeline<T>>,
63}
64
65impl<
66    T: Float
67        + core::iter::Sum<T>
68        + Copy
69        + 'static
70        + core::fmt::Debug
71        + num::Signed
72        + core::ops::AddAssign
73        + core::marker::Send
74        + core::marker::Sync
75        + core::ops::SubAssign
76        + Into<f64>
77        + num::FromPrimitive,
78> TimeQueue<T>
79where
80    Box<dyn AnalysisPipeline<T>>: Default,
81{
82    /// Creates a new `TimeQueue` with a sliding window of size `time`. A default pipeline is used.
83    ///
84    /// # Arguments
85    /// * `time` - The length of the time window (in ms).
86    pub fn new(time: usize) -> Self {
87        Self {
88            data: VecDeque::with_capacity(4 * time), // At most 250 bpm
89            time: T::from(time).unwrap(),
90            current_time: T::from(0).unwrap(),
91            pipeline: Default::default(),
92        }
93    }
94
95    /// Sets a custom pipeline to be used for HRV calculation.
96    ///
97    /// # Arguments
98    /// * `pipeline` - A boxed `AnalysisPipeline` that implements the HRV processing logic.
99    pub fn set_pipeline(&mut self, pipeline: Box<dyn AnalysisPipeline<T>>) {
100        self.pipeline = pipeline;
101    }
102
103    /// Pushes a new RR interval to the queue. The queue maintains the sliding window of RR intervals.
104    /// Once the current time exceeds the defined window time, the oldest interval is removed.
105    pub fn push(&mut self, rr_interval: T) {
106        self.current_time += rr_interval;
107        self.data.push_back(rr_interval);
108        if self.current_time >= self.time {
109            if let Some(deleted) = self.data.pop_front() {
110                self.current_time -= deleted;
111            }
112        }
113    }
114
115    /// Returns the current slice of the stored RR intervals.
116    pub fn get(&self) -> &[T] {
117        self.data.as_slices().0
118    }
119
120    /// Processes the current RR intervals using the pipeline and returns the HRV metrics.
121    ///
122    /// This function calls the `process` method on the custom or default pipeline to calculate HRV.
123    pub fn get_hrv(&self) -> crate::HrvMetrics<T> {
124        self.pipeline.process(self.get().to_vec())
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::{
132        frequency_domain::FrequencyMetrics, geometric_domain::GeometricMetrics,
133        non_linear::NonLinearMetrics, processing_utils::RRIntervals, time_domain::TimeMetrics,
134        utils::test_data::RR_INTERVALS,
135    };
136    #[cfg(not(feature = "std"))]
137    use alloc::vec::Vec;
138
139    #[test]
140    fn test_queue() {
141        let mut data = RR_INTERVALS.to_vec();
142        data.extend_from_within(..);
143        let mut queue = TimeQueue::new(298_500usize);
144        let mut win_0: crate::HrvMetrics<f64> = Default::default();
145        for (i, j) in data.iter().enumerate() {
146            queue.push(*j);
147            if i == data.len() / 2 - 1 {
148                win_0 = queue.get_hrv();
149            }
150        }
151
152        let win_1 = queue.get_hrv();
153        assert_eq!(win_0, win_1);
154    }
155    #[test]
156    fn test_queue_custom_pipeline() {
157        let mut data = RR_INTERVALS.to_vec();
158        data.extend_from_within(..);
159        let mut queue = TimeQueue::new(298_500usize);
160
161        struct Pipeline();
162        impl AnalysisPipeline<f64> for Pipeline {
163            fn process(&self, data: Vec<f64>) -> crate::HrvMetrics<f64> {
164                let rr_intervals = RRIntervals::new(data);
165
166                let time = TimeMetrics::compute(rr_intervals.as_slice());
167                let frequency = FrequencyMetrics::compute(rr_intervals.as_slice(), 4.);
168                let geometric = GeometricMetrics::compute(rr_intervals.as_slice());
169                let non_linear = NonLinearMetrics::compute_default(rr_intervals.as_slice());
170
171                crate::HrvMetrics {
172                    time,
173                    frequency,
174                    geometric,
175                    non_linear,
176                }
177            }
178        }
179        queue.set_pipeline(Box::new(Pipeline()));
180
181        let mut win_0: crate::HrvMetrics<f64> = Default::default();
182        for (i, j) in data.iter().enumerate() {
183            queue.push(*j);
184            if i == data.len() / 2 - 1 {
185                win_0 = queue.get_hrv();
186            }
187        }
188
189        let win_1 = queue.get_hrv();
190        assert_eq!(win_0, win_1);
191    }
192}