Skip to main content

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}