Skip to content

Commit c17dd21

Browse files
authored
Clonable ResamplingFunctions (#69)
2 parents 4371270 + 3e2cbfa commit c17dd21

File tree

5 files changed

+116
-49
lines changed

5 files changed

+116
-49
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Frequenz Resampling Release Notes
22

3-
## Summary
3+
## New Features
44

5-
This is the first release of Frequenz Resampling.
5+
- `ResamplingFunction` now implements `Clone`.

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,7 @@ mod tests;
8181
#[cfg(feature = "python")]
8282
mod python;
8383

84-
pub use resampler::{Resampler, ResamplingFunction, Sample};
84+
mod resampling_function;
85+
pub use resampling_function::ResamplingFunction;
86+
87+
pub use resampler::{Resampler, Sample};

src/resampler.rs

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44
//! The resampler module provides the Resampler struct that is used to resample
55
//! a time series of samples.
66
7+
use crate::ResamplingFunction;
78
use chrono::{DateTime, TimeDelta, Utc};
9+
use itertools::Itertools;
810
use log::warn;
911
use num_traits::FromPrimitive;
1012
use std::fmt::Debug;
1113
use std::ops::Div;
1214

13-
use itertools::Itertools;
14-
15-
pub type CustomResamplingFunction<S, T> = Box<dyn FnMut(&[&S]) -> Option<T> + Send + Sync>;
16-
1715
/// The Sample trait represents a single sample in a time series.
1816
pub trait Sample: Clone + Debug + Default {
1917
type Value;
@@ -22,42 +20,6 @@ pub trait Sample: Clone + Debug + Default {
2220
fn value(&self) -> Option<Self::Value>;
2321
}
2422

25-
/// The ResamplingFunction enum represents the different resampling functions
26-
/// that can be used to resample a channel.
27-
#[derive(Default)]
28-
pub enum ResamplingFunction<
29-
T: Div<Output = T> + std::iter::Sum + Default + Debug,
30-
S: Sample<Value = T>,
31-
> {
32-
/// Calculates the average of all samples in the time step (ignoring None
33-
/// values)
34-
#[default]
35-
Average,
36-
/// Calculates the sum of all samples in the time step (ignoring None
37-
/// values)
38-
Sum,
39-
/// Calculates the maximum value of all samples in the time step (ignoring
40-
/// None values)
41-
Max,
42-
/// Calculates the minimum value of all samples in the time step (ignoring
43-
/// None values)
44-
Min,
45-
/// Uses the first sample in the time step. If the first sample is None, the
46-
/// resampling function will return None.
47-
First,
48-
/// Uses the last sample in the time step. If the last sample is None, the
49-
/// resampling function will return None.
50-
Last,
51-
/// Returns the first non-None sample in the time step. If all samples are
52-
/// None, the resampling function will return None.
53-
Coalesce,
54-
/// Counts the number of samples in the time step (ignoring None values)
55-
Count,
56-
/// A custom resampling function that takes a closure that takes a slice of
57-
/// samples and returns an optional value.
58-
Custom(CustomResamplingFunction<S, T>),
59-
}
60-
6123
impl<
6224
T: Div<Output = T> + std::iter::Sum + PartialOrd + FromPrimitive + Default + Debug,
6325
S: Sample<Value = T>,

src/resampling_function.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// License: MIT
2+
// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
//! Resampling functions for time series data.
5+
6+
use crate::Sample;
7+
use std::{fmt::Debug, ops::Div};
8+
9+
// This trait can't directly have `Clone` as a supertrait because
10+
// then it wouldn't be dyn-compatible.
11+
//
12+
// Instead, we make a blanket implementation for all FnMut that implement
13+
// `Clone`, which will be dispatched statically.
14+
pub trait ClonableFnMut<S, T>: FnMut(&[&S]) -> Option<T> + Send + Sync {
15+
fn clone_box(&self) -> Box<dyn ClonableFnMut<S, T>>;
16+
}
17+
18+
impl<S, T, F> ClonableFnMut<S, T> for F
19+
where
20+
F: FnMut(&[&S]) -> Option<T> + Send + Sync + Clone + 'static,
21+
{
22+
fn clone_box(&self) -> Box<dyn ClonableFnMut<S, T>> {
23+
Box::new(self.clone())
24+
}
25+
}
26+
27+
/// The ResamplingFunction enum represents the different resampling functions
28+
/// that can be used to resample a channel.
29+
#[derive(Default)]
30+
pub enum ResamplingFunction<
31+
T: Div<Output = T> + std::iter::Sum + Default + Debug,
32+
S: Sample<Value = T>,
33+
> {
34+
/// Calculates the average of all samples in the time step (ignoring None
35+
/// values)
36+
#[default]
37+
Average,
38+
/// Calculates the sum of all samples in the time step (ignoring None
39+
/// values)
40+
Sum,
41+
/// Calculates the maximum value of all samples in the time step (ignoring
42+
/// None values)
43+
Max,
44+
/// Calculates the minimum value of all samples in the time step (ignoring
45+
/// None values)
46+
Min,
47+
/// Uses the first sample in the time step. If the first sample is None, the
48+
/// resampling function will return None.
49+
First,
50+
/// Uses the last sample in the time step. If the last sample is None, the
51+
/// resampling function will return None.
52+
Last,
53+
/// Returns the first non-None sample in the time step. If all samples are
54+
/// None, the resampling function will return None.
55+
Coalesce,
56+
/// Counts the number of samples in the time step (ignoring None values)
57+
Count,
58+
/// A custom resampling function that takes a closure that takes a slice of
59+
/// samples and returns an optional value.
60+
Custom(Box<dyn ClonableFnMut<S, T>>),
61+
}
62+
63+
impl<T, S> Clone for ResamplingFunction<T, S>
64+
where
65+
T: Div<Output = T> + std::iter::Sum + Default + Debug,
66+
S: Sample<Value = T>,
67+
{
68+
fn clone(&self) -> Self {
69+
match self {
70+
Self::Average => Self::Average,
71+
Self::Sum => Self::Sum,
72+
Self::Max => Self::Max,
73+
Self::Min => Self::Min,
74+
Self::First => Self::First,
75+
Self::Last => Self::Last,
76+
Self::Coalesce => Self::Coalesce,
77+
Self::Count => Self::Count,
78+
Self::Custom(f) => Self::Custom(f.clone_box()),
79+
}
80+
}
81+
}

src/tests.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use std::{
99
ops::{Add, Div},
1010
};
1111

12-
use crate::resampler::{epoch_align, Resampler, ResamplingFunction, Sample};
12+
use crate::resampler::{epoch_align, Resampler, Sample};
13+
use crate::ResamplingFunction;
1314
use chrono::{DateTime, TimeDelta, Utc};
1415
use num_traits::FromPrimitive;
1516

@@ -334,13 +335,33 @@ fn test_resampling_coalesce() {
334335

335336
#[test]
336337
fn test_resampling_custom() {
338+
// Create a custom FnMut with internal state (an increment).
339+
let mut increment = 0.0;
340+
let resampling_fn = ResamplingFunction::Custom(Box::new(move |x: &[&TestSample]| {
341+
increment += 1.0;
342+
Some(x.iter().map(|s| s.value().unwrap()).sum::<f64>() + increment)
343+
}));
344+
337345
test_resampling(
338-
ResamplingFunction::Custom(Box::new(|x: &[&TestSample]| {
339-
Some(x.iter().map(|s| s.value().unwrap()).sum::<f64>())
340-
})),
346+
resampling_fn.clone(),
341347
vec![
342-
TestSample::new(DateTime::from_timestamp(5, 0).unwrap(), Some(15.0)),
343-
TestSample::new(DateTime::from_timestamp(10, 0).unwrap(), Some(40.0)),
348+
TestSample::new(DateTime::from_timestamp(5, 0).unwrap(), Some(16.0)),
349+
TestSample::new(DateTime::from_timestamp(10, 0).unwrap(), Some(42.0)),
350+
],
351+
);
352+
// Ensure that clones don't share mutable state with the original.
353+
test_resampling(
354+
resampling_fn.clone(),
355+
vec![
356+
TestSample::new(DateTime::from_timestamp(5, 0).unwrap(), Some(16.0)),
357+
TestSample::new(DateTime::from_timestamp(10, 0).unwrap(), Some(42.0)),
358+
],
359+
);
360+
test_resampling(
361+
resampling_fn,
362+
vec![
363+
TestSample::new(DateTime::from_timestamp(5, 0).unwrap(), Some(16.0)),
364+
TestSample::new(DateTime::from_timestamp(10, 0).unwrap(), Some(42.0)),
344365
],
345366
);
346367
}

0 commit comments

Comments
 (0)