frequenz_resampling/lib.rs
1// License: MIT
2// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
4/*!
5# frequenz-resampling-rs
6
7This project is the rust resampler for resampling a stream of samples to a
8given interval.
9
10## Usage
11
12An instance of the [`Resampler`] can be created with the
13[`new`][Resampler::new] method.
14Raw data can be added to the resampler either through the
15[`push`][Resampler::push] or [`extend`][Resampler::extend] methods, and the
16[`resample`][Resampler::resample] method resamples the data that was added to
17the buffer.
18
19```rust
20use chrono::{DateTime, TimeDelta, Utc};
21use frequenz_resampling::{Closed, Label, Resampler, ResamplingFunction, Sample};
22
23#[derive(Debug, Clone, Default, Copy, PartialEq)]
24pub(crate) struct TestSample {
25 timestamp: DateTime<Utc>,
26 value: Option<f64>,
27}
28
29impl Sample for TestSample {
30 type Value = f64;
31
32 fn new(timestamp: DateTime<Utc>, value: Option<f64>) -> Self {
33 Self { timestamp, value }
34 }
35
36 fn timestamp(&self) -> DateTime<Utc> {
37 self.timestamp
38 }
39
40 fn value(&self) -> Option<f64> {
41 self.value
42 }
43}
44
45let start = DateTime::from_timestamp(0, 0).unwrap();
46let mut resampler: Resampler<f64, TestSample> =
47 Resampler::new(
48 TimeDelta::seconds(5),
49 ResamplingFunction::Average,
50 1,
51 start,
52 Closed::Left,
53 Label::Right,
54 );
55
56let step = TimeDelta::seconds(1);
57// Data starts at t=0 with values 1-10
58// Interval [0, 5): t=0,1,2,3,4 with values 1,2,3,4,5 → avg = 3.0
59// Interval [5, 10): t=5,6,7,8,9 with values 6,7,8,9,10 → avg = 8.0
60let data = vec![
61 TestSample::new(start, Some(1.0)),
62 TestSample::new(start + step, Some(2.0)),
63 TestSample::new(start + step * 2, Some(3.0)),
64 TestSample::new(start + step * 3, Some(4.0)),
65 TestSample::new(start + step * 4, Some(5.0)),
66 TestSample::new(start + step * 5, Some(6.0)),
67 TestSample::new(start + step * 6, Some(7.0)),
68 TestSample::new(start + step * 7, Some(8.0)),
69 TestSample::new(start + step * 8, Some(9.0)),
70 TestSample::new(start + step * 9, Some(10.0)),
71];
72
73resampler.extend(data);
74
75let resampled = resampler.resample(start + step * 10);
76
77let expected = vec![
78 TestSample::new(DateTime::from_timestamp(5, 0).unwrap(), Some(3.0)),
79 TestSample::new(DateTime::from_timestamp(10, 0).unwrap(), Some(8.0)),
80];
81
82assert_eq!(resampled, expected);
83```
84*/
85
86mod resampler;
87
88#[cfg(test)]
89mod tests;
90
91#[cfg(feature = "python")]
92mod python;
93
94mod resampling_function;
95pub use resampling_function::ResamplingFunction;
96
97pub use resampler::{epoch_align, Closed, Label, Resampler, Sample};
98
99use chrono::{DateTime, TimeDelta, Utc};
100
101/// A simple sample type for use with the `resample` function.
102#[derive(Default, Clone, Debug, Copy, PartialEq)]
103pub struct SimpleSample {
104 timestamp: DateTime<Utc>,
105 value: Option<f64>,
106}
107
108impl Sample for SimpleSample {
109 type Value = f64;
110
111 fn new(timestamp: DateTime<Utc>, value: Option<f64>) -> Self {
112 Self { timestamp, value }
113 }
114
115 fn timestamp(&self) -> DateTime<Utc> {
116 self.timestamp
117 }
118
119 fn value(&self) -> Option<f64> {
120 self.value
121 }
122}
123
124/// Resamples a list of timestamp/value pairs in a single call.
125///
126/// This is a convenience function for one-shot resampling without needing to
127/// manage a `Resampler` instance.
128///
129/// # Arguments
130///
131/// * `data` - A slice of (timestamp, value) tuples to resample. Must be sorted by timestamp.
132/// * `interval` - The resampling interval.
133/// * `resampling_function` - The function to use for aggregating values within each interval.
134/// * `closed` - Controls which edge of the interval is closed for sample membership.
135/// Use [`Closed::Left`] for `[start, end)` intervals or [`Closed::Right`] for
136/// `(start, end]` intervals.
137/// * `label` - Controls which edge of the interval is used for output timestamps.
138/// Use [`Label::Left`] for the start of each interval or [`Label::Right`] for
139/// the end of each interval.
140///
141/// # Returns
142///
143/// A vector of (timestamp, value) tuples representing the resampled data.
144///
145/// The helper mirrors pandas-style bucket coverage for the input range. In
146/// particular, with [`Closed::Right`], it includes the leading bucket that
147/// ends exactly at the first sample timestamp when that timestamp lies on an
148/// interval boundary.
149///
150/// # Example
151///
152/// ```rust
153/// use chrono::{DateTime, TimeDelta, Utc};
154/// use frequenz_resampling::{resample, Closed, Label, ResamplingFunction};
155///
156/// let start = DateTime::from_timestamp(0, 0).unwrap();
157/// let step = TimeDelta::seconds(1);
158/// let data: Vec<(DateTime<Utc>, Option<f64>)> = (0..10)
159/// .map(|i| (start + step * i, Some((i + 1) as f64)))
160/// .collect();
161///
162/// let result = resample(
163/// &data,
164/// TimeDelta::seconds(5),
165/// ResamplingFunction::Average,
166/// Closed::Left,
167/// Label::Left,
168/// );
169/// // Result: [(t=0, 3.0), (t=5, 8.0)]
170/// assert_eq!(result.len(), 2);
171/// assert_eq!(result[0].1, Some(3.0));
172/// assert_eq!(result[1].1, Some(8.0));
173/// ```
174pub fn resample(
175 data: &[(DateTime<Utc>, Option<f64>)],
176 interval: TimeDelta,
177 resampling_function: ResamplingFunction<f64, SimpleSample>,
178 closed: Closed,
179 label: Label,
180) -> Vec<(DateTime<Utc>, Option<f64>)> {
181 let (Some(first_ts), Some(last_ts)) = (
182 data.first().map(|(ts, _)| *ts),
183 data.last().map(|(ts, _)| *ts),
184 ) else {
185 return vec![];
186 };
187
188 let aligned_start = epoch_align(interval, first_ts, None);
189 let aligned_end = epoch_align(interval, last_ts, None);
190 let start = if closed == Closed::Right && first_ts == aligned_start {
191 aligned_start - interval
192 } else {
193 aligned_start
194 };
195 let end = if closed == Closed::Right && last_ts == aligned_end {
196 aligned_end
197 } else {
198 aligned_end + interval
199 };
200
201 let mut resampler: Resampler<f64, SimpleSample> =
202 Resampler::new(interval, resampling_function, 1, start, closed, label);
203 resampler.extend(data.iter().map(|(ts, val)| SimpleSample::new(*ts, *val)));
204 resampler
205 .resample(end)
206 .into_iter()
207 .map(|s| (s.timestamp(), s.value()))
208 .collect()
209}