//! Concrete signal types //! //! In Argus, there are essentially 2 kinds of signals: //! //! 1. [`Signal`] is a variable length signal with finitely many sampled points. This //! implies that the signal has a fixed start and end point (both inclusive) and can //! be iterated over. //! 2. [`ConstantSignal`] is a signal that maintains a constant value throughtout //! its domain, and thus, do not require interpolation and extrapolation. Moreover, //! since they are defined over the entire time domain, they cannot be iterated over. pub mod bool_ops; pub mod cmp_ops; pub mod iter; pub mod num_ops; pub mod traits; mod utils; use std::ops::{RangeFull, RangeInclusive}; use std::time::Duration; pub use bool_ops::*; pub use cmp_ops::*; pub use num_ops::*; use self::traits::{BaseSignal, LinearInterpolatable}; use crate::{ArgusResult, Error}; /// All supported signal types in Argus pub enum AnySignal { Bool(Signal), ConstBool(ConstantSignal), Int(Signal), ConstInt(ConstantSignal), UInt(Signal), ConstUInt(ConstantSignal), Float(Signal), ConstFloat(ConstantSignal), } #[derive(Debug, Clone, Copy)] pub enum InterpolationMethod { Linear, Nearest, } impl InterpolationMethod { pub(crate) fn at(self, time: Duration, a: &Option>, b: &Option>) -> Option 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, } } } #[derive(Copy, Clone, Debug)] pub struct Sample { pub time: Duration, pub value: T, } /// A signal is a sequence of time points ([`Duration`](core::time::Duration)) and /// corresponding value samples. #[derive(Default, Debug, Clone)] pub struct Signal { pub(crate) values: Vec, pub(crate) time_points: Vec, } impl Signal { /// Create a new empty signal pub fn new() -> Self { Self { values: Default::default(), time_points: Default::default(), } } /// Create a new empty signal with the specified capacity pub fn new_with_capacity(size: usize) -> Self { Self { values: Vec::with_capacity(size), time_points: Vec::with_capacity(size), } } /// Create an iterator over the pairs of time points and values of the signal. pub fn iter(&self) -> impl Iterator { self.into_iter() } /// Try to create a signal from the input iterator /// /// Returns an `Err` if the input samples are not in strictly monotonically /// increasing order. pub fn try_from_iter(iter: I) -> ArgusResult where I: IntoIterator, { let iter = iter.into_iter(); let mut signal = Signal::new_with_capacity(iter.size_hint().0); for (time, value) in iter.into_iter() { signal.push(time, value)?; } Ok(signal) } } impl BaseSignal for Signal { type Value = T; type Bounds = RangeInclusive; fn at(&self, time: Duration) -> Option<&Self::Value> { assert_eq!( self.time_points.len(), self.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 self.time_points.is_empty() { return None; } // We will use binary search to find the appropriate index match self.time_points.binary_search(&time) { Ok(idx) => self.values.get(idx), Err(_) => None, } } fn interpolate_at(&self, time: Duration, interp: InterpolationMethod) -> Option where Self::Value: Copy + LinearInterpolatable, { assert_eq!( self.time_points.len(), self.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 self.time_points.is_empty() { return None; } // We will use binary search to find the appropriate index let hint_idx = match self.time_points.binary_search(&time) { Ok(idx) => return self.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: self.time_points[hint_idx], value: self.values[hint_idx], }); (preceding, following) } else if hint_idx == self.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: self.time_points[hint_idx - 1], value: self.values[hint_idx - 1], }); let following = None; (preceding, following) } else { // The sample should exist within the signal. assert!(self.time_points.len() >= 2, "There should be at least 2 elements"); let preceding = Some(Sample { time: self.time_points[hint_idx - 1], value: self.values[hint_idx - 1], }); let following = Some(Sample { time: self.time_points[hint_idx], value: self.values[hint_idx], }); (preceding, following) }; interp.at(time, &first, &second) } fn bounds(&self) -> Self::Bounds { let first = self.time_points.first(); let last = self.time_points.last(); match (first, last) { (None, None) => Duration::from_secs(1)..=Duration::from_secs(0), (Some(first), Some(last)) => *first..=*last, (..) => unreachable!("there is either 0 time points or some time points"), } } fn push(&mut self, time: Duration, value: Self::Value) -> ArgusResult { assert_eq!(self.time_points.len(), self.values.len()); let last_time = self.time_points.last(); match last_time { Some(last_t) if last_t >= &time => Err(Error::NonMonotonicSignal { end_time: *last_t, current_sample: time, }), _ => { self.time_points.push(time); self.values.push(value); Ok(true) } } } } #[derive(Debug, Clone)] pub struct ConstantSignal { pub value: T, } impl ConstantSignal { pub fn new(value: T) -> Self { Self { value } } } impl BaseSignal for ConstantSignal { type Value = T; type Bounds = RangeFull; fn at(&self, _time: Duration) -> Option<&Self::Value> { Some(&self.value) } fn bounds(&self) -> Self::Bounds { .. } fn interpolate_at(&self, _time: Duration, _interp: InterpolationMethod) -> Option where Self::Value: Copy + LinearInterpolatable, { Some(self.value) } fn push(&mut self, _time: Duration, _value: Self::Value) -> ArgusResult { Ok(false) } } #[cfg(test)] pub mod arbitrary { use itertools::Itertools; use proptest::prelude::*; use proptest::sample::SizeRange; use super::*; /// Generate an arbitrary list of samples and two indices within the list pub fn samples_and_indices( size: impl Into, ) -> impl Strategy, usize, usize)> where T: Arbitrary + Copy, { samples(size).prop_flat_map(|vec| { let len = vec.len(); if len == 0 { (Just(vec), 0..1, 0..1) } else { (Just(vec), 0..len, 0..len) } }) } /// Generate arbitrary samples for a signal where the time stamps are strictly /// monotonically increasing pub fn samples(size: impl Into) -> impl Strategy> where T: Arbitrary + Copy, { prop::collection::vec(any::(), size).prop_flat_map(|values| { let len = values.len(); prop::collection::vec(any::(), len).prop_map(move |mut ts| { ts.sort_unstable(); ts.dedup(); ts.into_iter() .map(Duration::from_secs) .zip(values.clone().into_iter()) .collect_vec() }) }) } /// Generate arbitrary finite-length signals with samples of the given type pub fn sampled_signal(size: impl Into) -> impl Strategy> where T: Arbitrary + Copy, { samples(size).prop_map(Signal::::from_iter) } /// Generate an arbitrary constant signal pub fn constant_signal() -> impl Strategy> where T: Arbitrary, { any::().prop_map(ConstantSignal::new) } } #[cfg(test)] mod tests { use core::ops::Bound; use paste::paste; use proptest::prelude::*; use super::*; macro_rules! correctly_create_signals_impl { ($ty:ty) => { proptest! { |((samples, idx, _) in arbitrary::samples_and_indices::<$ty>(0..100))| { // Creating a signal should be fine let signal: Signal<_> = samples.clone().into_iter().collect(); if samples.len() > 0 { // We wil get the start and end times. let start_time = samples.first().unwrap().0; let end_time = samples.last().unwrap().0; // Get the value of the sample at a given index let (at, val) = samples[idx]; assert_eq!(signal.start_time(), Bound::Included(start_time)); assert_eq!(signal.end_time(), Bound::Included(end_time)); assert_eq!(signal.at(at), Some(&val)); assert_eq!(signal.at(end_time + Duration::from_secs(1)), None); assert_eq!(signal.at(start_time - Duration::from_secs(1)), None); } else { assert!(signal.is_empty()); assert_eq!(signal.at(Duration::from_secs(1)), None); } } } proptest! { |((mut samples, a, b) in arbitrary::samples_and_indices::<$ty>(5..100))| { prop_assume!(a != b); // Swap two indices in the samples samples.swap(a, b); // Creating a signal should fail let signal = Signal::try_from_iter(samples.clone()); assert!(signal.is_err(), "swapped {:?} and {:?}", samples[a], samples[b]); } } }; } #[test] fn create_signals_from_samples() { correctly_create_signals_impl!(bool); correctly_create_signals_impl!(i8); correctly_create_signals_impl!(i16); correctly_create_signals_impl!(i32); correctly_create_signals_impl!(i64); correctly_create_signals_impl!(u8); correctly_create_signals_impl!(u16); correctly_create_signals_impl!(u32); correctly_create_signals_impl!(u64); correctly_create_signals_impl!(f32); correctly_create_signals_impl!(f64); } macro_rules! signals_fromiter_panic { ($ty:ty) => { paste! { proptest! { #[test] #[should_panic] fn [] ((mut samples, a, b) in arbitrary::samples_and_indices::<$ty>(5..100)) { prop_assume!(a != b); // Swap two indices in the samples samples.swap(a, b); // Creating a signal should fail let _: Signal<_> = samples.into_iter().collect(); } } } }; } signals_fromiter_panic!(bool); signals_fromiter_panic!(i8); signals_fromiter_panic!(i16); signals_fromiter_panic!(i32); signals_fromiter_panic!(i64); signals_fromiter_panic!(u8); signals_fromiter_panic!(u16); signals_fromiter_panic!(u32); signals_fromiter_panic!(u64); signals_fromiter_panic!(f32); signals_fromiter_panic!(f64); macro_rules! signal_ops_impl { ($ty:ty, $op:tt sig) => { proptest! { |(sig in arbitrary::sampled_signal::<$ty>(1..100))| { use InterpolationMethod::Linear; let new_sig = $op (&sig); for (t, v) in new_sig.iter() { let prev = sig.interpolate_at(*t, Linear).unwrap(); assert_eq!($op prev, *v); } } } }; ($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; 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(); assert_eq!(v1 $op v2, *v); } } } proptest! { |(sig1 in arbitrary::sampled_signal::<$ty>(1..100), sig2 in arbitrary::constant_signal::<$ty>())| { use InterpolationMethod::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(); assert_eq!(v1 $op v2, *v); } } } proptest! { |(sig1 in arbitrary::constant_signal::<$ty>(), sig2 in arbitrary::constant_signal::<$ty>())| { let new_sig = &sig1 $op &sig2; let v1 = sig1.value; let v2 = sig2.value; let v = new_sig.value; assert_eq!(v1 $op v2, v); } } }; } #[test] fn signal_ops() { signal_ops_impl!(bool, !sig); signal_ops_impl!(bool, lhs | rhs); signal_ops_impl!(bool, lhs & rhs); // signal_ops_impl!(u64, lhs + rhs); // signal_ops_impl!(u64, lhs * rhs); signal_ops_impl!(u64, lhs / rhs); signal_ops_impl!(i64, -sig); // signal_ops_impl!(i64, lhs + rhs); // signal_ops_impl!(i64, lhs * rhs); signal_ops_impl!(i64, lhs / rhs); signal_ops_impl!(f32, -sig); signal_ops_impl!(f32, lhs + rhs); signal_ops_impl!(f32, lhs * rhs); // signal_ops_impl!(f32, lhs / rhs); signal_ops_impl!(f64, -sig); signal_ops_impl!(f64, lhs + rhs); signal_ops_impl!(f64, lhs * rhs); // signal_ops_impl!(f64, lhs / rhs); } }