refactor~(core): use traits and structs for interpolation

We have to now pass the interpolation method as a generic argument to methods.
This commit is contained in:
Anand Balakrishnan 2023-06-07 09:57:56 -04:00
parent 2b16ef9c40
commit 87afc11b90
No known key found for this signature in database
8 changed files with 314 additions and 306 deletions

View file

@ -2,6 +2,7 @@
mod bool_ops;
mod cast;
mod cmp_ops;
pub mod interpolation;
pub mod iter;
mod num_ops;
mod shift_ops;
@ -23,43 +24,6 @@ use utils::intersect_bounds;
use crate::{ArgusResult, Error};
/// Interpolation methods supported by Argus signals.
///
/// Defaults to `Linear` interpolation (which corresponds to constant interpolation for
/// `bool` signals).
#[derive(Debug, Clone, Copy, Default)]
pub enum InterpolationMethod {
/// Linear interpolation
#[default]
Linear,
/// Interpolate from the nearest sample point.
Nearest,
}
impl InterpolationMethod {
pub(crate) fn at<T>(self, time: Duration, a: &Option<Sample<T>>, b: &Option<Sample<T>>) -> Option<T>
where
T: Copy + LinearInterpolatable,
{
use InterpolationMethod::*;
match (self, a, b) {
(Nearest, Some(ref a), Some(ref b)) => {
assert!(a.time < time && time < b.time);
if (b.time - time) > (time - a.time) {
// a is closer to the required time than b
Some(a.value)
} else {
// b is closer
Some(b.value)
}
}
(Nearest, Some(nearest), None) | (Nearest, None, Some(nearest)) => Some(nearest.value),
(Linear, Some(a), Some(b)) => Some(T::interpolate_at(a, b, time)),
_ => None,
}
}
}
/// A single sample of a signal.
#[derive(Copy, Clone, Debug)]
pub struct Sample<T> {
@ -241,80 +205,6 @@ impl<T> Signal<T> {
}
}
/// Interpolate the value of the signal at the given time point
///
/// If there exists a sample at the given time point then `Some(value)` is returned
/// with the value of the signal at the point. Otherwise, a the
/// [`InterpolationMethod`] is used to compute the value. If the given interpolation
/// method cannot be used at the given time (for example, if we use
/// [`InterpolationMethod::Linear`] and the `time` point is outside the signal
/// domain), then a `None` is returned.
pub fn interpolate_at(&self, time: Duration, interp: InterpolationMethod) -> Option<T>
where
T: Copy + LinearInterpolatable,
{
match self {
Signal::Empty => None,
Signal::Constant { value } => Some(*value),
Signal::Sampled { values, time_points } => {
assert_eq!(
time_points.len(),
values.len(),
"invariant: number of time points must equal number of samples"
);
// if there are no sample points, then there is no sample point (nor neighboring
// sample points) to return
if time_points.is_empty() {
return None;
}
// We will use binary search to find the appropriate index
let hint_idx = match time_points.binary_search(&time) {
Ok(idx) => return values.get(idx).copied(),
Err(idx) => idx,
};
// We have an hint as to where the sample _should have been_.
// So, lets check if there is a preceding and/or following sample.
let (first, second) = if hint_idx == 0 {
// Sample appears before the start of the signal
// So, let's return just the following sample, which is the first sample
// (since we know that the signal is non-empty).
let preceding = None;
let following = Some(Sample {
time: time_points[hint_idx],
value: values[hint_idx],
});
(preceding, following)
} else if hint_idx == time_points.len() {
// Sample appears past the end of the signal
// So, let's return just the preceding sample, which is the last sample
// (since we know the signal is non-empty)
let preceding = Some(Sample {
time: time_points[hint_idx - 1],
value: values[hint_idx - 1],
});
let following = None;
(preceding, following)
} else {
// The sample should exist within the signal.
assert!(time_points.len() >= 2, "There should be at least 2 elements");
let preceding = Some(Sample {
time: time_points[hint_idx - 1],
value: values[hint_idx - 1],
});
let following = Some(Sample {
time: time_points[hint_idx],
value: values[hint_idx],
});
(preceding, following)
};
interp.at(time, &first, &second)
}
}
}
/// Return the vector of points where the signal is sampled.
///
/// - If the signal is empty ([`Signal::Empty`]), the output is `None`.
@ -371,9 +261,10 @@ impl<T> Signal<T> {
}
/// Augment synchronization points with time points where signals intersect
pub fn sync_with_intersection(&self, other: &Signal<T>) -> Option<Vec<Duration>>
pub fn sync_with_intersection<Interp>(&self, other: &Signal<T>) -> Option<Vec<Duration>>
where
T: PartialOrd + Copy + LinearInterpolatable,
T: PartialOrd + Copy,
Interp: FindIntersectionMethod<T>,
{
use core::cmp::Ordering::*;
let sync_points: Vec<&Duration> = self.sync_points(other)?.into_iter().collect();
@ -405,7 +296,7 @@ impl<T> Signal<T> {
first: other.at(tm1).copied().map(|value| Sample { time: tm1, value }),
second: other.at(*t).copied().map(|value| Sample { time: *t, value }),
};
let intersect = T::find_intersection(&a, &b);
let intersect = Interp::find_intersection(&a, &b);
return_points.push(intersect.time);
}
}
@ -418,6 +309,70 @@ impl<T> Signal<T> {
}
}
impl<T: Copy> Signal<T> {
/// Interpolate the value of the signal at the given time point
///
/// If there exists a sample at the given time point then `Some(value)` is returned
/// with the value of the signal at the point. Otherwise, a the
/// [`InterpolationMethod`] is used to compute the value. If the given interpolation
/// method cannot be used at the given time (for example, if we use
/// [`interpolation::Linear`] and the `time` point is outside the signal
/// domain), then a `None` is returned.
pub fn interpolate_at<Interp>(&self, time: Duration) -> Option<T>
where
Interp: InterpolationMethod<T>,
{
match self {
Signal::Empty => None,
Signal::Constant { value } => Some(*value),
Signal::Sampled { values, time_points } => {
assert_eq!(
time_points.len(),
values.len(),
"invariant: number of time points must equal number of samples"
);
// if there are no sample points, then there is no sample point (nor neighboring
// sample points) to return
if time_points.is_empty() {
return None;
}
// We will use binary search to find the appropriate index
let hint_idx = match time_points.binary_search(&time) {
Ok(idx) => return values.get(idx).copied(),
Err(idx) => idx,
};
// We have an hint as to where the sample _should have been_.
// So, lets check if there is a preceding and/or following sample.
if hint_idx == 0 {
// Sample appears before the start of the signal
// So, let's return just the following sample, which is the first sample
// (since we know that the signal is non-empty).
Some(values[hint_idx])
} else if hint_idx == time_points.len() {
// Sample appears past the end of the signal
// So, let's return just the preceding sample, which is the last sample
// (since we know the signal is non-empty)
Some(values[hint_idx - 1])
} else {
// The sample should exist within the signal.
assert!(time_points.len() >= 2, "There should be at least 2 elements");
let first = Sample {
time: time_points[hint_idx - 1],
value: values[hint_idx - 1],
};
let second = Sample {
time: time_points[hint_idx],
value: values[hint_idx],
};
Interp::at(&first, &second, time)
}
}
}
}
}
impl<T: Num> Signal<T> {
/// Create a constant `0` signal
pub fn zero() -> Self {
@ -611,10 +566,10 @@ mod tests {
($ty:ty, $op:tt sig) => {
proptest! {
|(sig in arbitrary::sampled_signal::<$ty>(1..100))| {
use InterpolationMethod::Linear;
use interpolation::Linear;
let new_sig = $op (&sig);
for (t, v) in new_sig.iter() {
let prev = sig.interpolate_at(*t, Linear).unwrap();
let prev = sig.interpolate_at::<Linear>(*t).unwrap();
assert_eq!($op prev, *v);
}
}
@ -623,11 +578,11 @@ mod tests {
($ty:ty, lhs $op:tt rhs) => {
proptest! {
|(sig1 in arbitrary::sampled_signal::<$ty>(1..100), sig2 in arbitrary::sampled_signal::<$ty>(1..100))| {
use InterpolationMethod::Linear;
use interpolation::Linear;
let new_sig = &sig1 $op &sig2;
for (t, v) in new_sig.iter() {
let v1 = sig1.interpolate_at(*t, Linear).unwrap();
let v2 = sig2.interpolate_at(*t, Linear).unwrap();
let v1 = sig1.interpolate_at::<Linear>(*t).unwrap();
let v2 = sig2.interpolate_at::<Linear>(*t).unwrap();
assert_eq!(v1 $op v2, *v);
}
}
@ -635,11 +590,11 @@ mod tests {
proptest! {
|(sig1 in arbitrary::sampled_signal::<$ty>(1..100), sig2 in arbitrary::constant_signal::<$ty>())| {
use InterpolationMethod::Linear;
use interpolation::Linear;
let new_sig = &sig1 $op &sig2;
for (t, v) in new_sig.iter() {
let v1 = sig1.interpolate_at(*t, Linear).unwrap();
let v2 = sig2.interpolate_at(*t, Linear).unwrap();
let v1 = sig1.interpolate_at::<Linear>(*t).unwrap();
let v2 = sig2.interpolate_at::<Linear>(*t).unwrap();
assert_eq!(v1 $op v2, *v);
}
}