diff --git a/argus-core/src/lib.rs b/argus-core/src/lib.rs index 6e3eeba..4a38698 100644 --- a/argus-core/src/lib.rs +++ b/argus-core/src/lib.rs @@ -13,6 +13,8 @@ pub enum Error { #[error("insufficient number of arguments")] IncompleteArgs, + #[error("cannot push value to non-sampled signal")] + InvalidPushToSignal, #[error( "trying to create a non-monotonically signal, signal end time ({end_time:?}) > sample time point \ ({current_sample:?})" diff --git a/argus-core/src/prelude.rs b/argus-core/src/prelude.rs index 177624d..2b12a41 100644 --- a/argus-core/src/prelude.rs +++ b/argus-core/src/prelude.rs @@ -1,3 +1,3 @@ pub use crate::expr::{BoolExpr, Expr, ExprBuilder, ExprRef, NumExpr}; -pub use crate::signals::{AnySignal, ConstantSignal, Signal}; +pub use crate::signals::Signal; pub use crate::{ArgusError, ArgusResult}; diff --git a/argus-core/src/signals.rs b/argus-core/src/signals.rs index ca970e0..ef1febb 100644 --- a/argus-core/src/signals.rs +++ b/argus-core/src/signals.rs @@ -16,30 +16,20 @@ pub mod num_ops; pub mod traits; mod utils; -use std::ops::{RangeFull, RangeInclusive}; +use std::ops::{Bound, RangeBounds}; use std::time::Duration; pub use bool_ops::*; pub use cast::*; pub use cmp_ops::*; +use itertools::Itertools; pub use num_ops::*; +use num_traits::NumCast; +use utils::intersect_bounds; -use self::traits::{BaseSignal, LinearInterpolatable}; +use self::traits::LinearInterpolatable; use crate::{ArgusResult, Error}; -/// All supported signal types in Argus -#[derive(Debug, Clone, derive_more::From)] -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, @@ -76,31 +66,116 @@ pub struct Sample { 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, +/// A typed Signal +/// +/// A Signal can either be empty, constant throughout its domain, or sampled at a +/// finite set of strictly monotonically increasing time points. +#[derive(Default, Clone, Debug)] +pub enum Signal { + #[default] + Empty, + Constant { + value: T, + }, + Sampled { + values: Vec, + time_points: Vec, + }, } impl Signal { /// Create a new empty signal pub fn new() -> Self { - Self { - values: Default::default(), - time_points: Default::default(), - } + Self::Empty + } + + /// Create a new constant signal + pub fn constant(value: T) -> Self { + Self::Constant { value } } /// Create a new empty signal with the specified capacity pub fn new_with_capacity(size: usize) -> Self { - Self { + Self::Sampled { values: Vec::with_capacity(size), time_points: Vec::with_capacity(size), } } + /// Get the bounds of the signal. + /// + /// Returns `None` if the signal is empty (either [`Signal::Empty`] or + /// [`Signal::Sampled`] with no samples. + pub fn bounds(&self) -> Option<(Bound, Bound)> { + use core::ops::Bound::*; + match self { + Signal::Empty => None, + Signal::Constant { value: _ } => Some((Unbounded, Unbounded)), + Signal::Sampled { values: _, time_points } => { + if time_points.is_empty() { + None + } else { + Some((Included(time_points[0]), Included(*time_points.last().unwrap()))) + } + } + } + } + + /// Check if the signal is empty + pub fn is_empty(&self) -> bool { + use core::ops::Bound::*; + let bounds = match self.bounds() { + Some(b) => b, + None => return true, + }; + match (bounds.start_bound(), bounds.end_bound()) { + (Included(start), Included(end)) => start > end, + (Included(start), Excluded(end)) | (Excluded(start), Included(end)) | (Excluded(start), Excluded(end)) => { + start >= end + } + + (Unbounded, Unbounded) => false, + bound => unreachable!("Argus doesn't support signals with bound {:?}", bound), + } + } + + /// Get the time at which the given signal starts. + pub fn start_time(&self) -> Option> { + self.bounds().map(|b| b.0) + } + + /// Get the time at which the given signal ends. + pub fn end_time(&self) -> Option> { + self.bounds().map(|b| b.1) + } + + /// Push a new sample to the signal at the given time point + /// + /// The method enforces the invariant that the time points of the signal must have + /// strictly monotonic increasing values, otherwise it returns an error without + /// adding the sample point. + /// Moreover, it is an error to `push` a value to an [`Empty`](Signal::Empty) or + /// [`Constant`](Signal::Constant) signal. + pub fn push(&mut self, time: Duration, value: T) -> ArgusResult<()> { + match self { + Signal::Empty | Signal::Constant { value: _ } => Err(Error::InvalidPushToSignal), + Signal::Sampled { values, time_points } => { + let last_time = time_points.last(); + match last_time { + Some(last_t) if last_t >= &time => Err(Error::NonMonotonicSignal { + end_time: *last_t, + current_sample: time, + }), + _ => { + time_points.push(time); + values.push(value); + Ok(()) + } + } + } + } + } + /// Create an iterator over the pairs of time points and values of the signal. pub fn iter(&self) -> impl Iterator { self.into_iter() @@ -121,152 +196,202 @@ impl Signal { } Ok(signal) } -} -impl BaseSignal for Signal { - type Value = T; - type Bounds = RangeInclusive; + /// Get 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. + /// Otherwise, `None` is returned. If the goal is to interpolate the value at the + /// a given time, see [`interpolate_at`](Self::interpolate_at). + pub fn at(&self, time: Duration) -> Option<&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; + } - 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) + // We will use binary search to find the appropriate index + match time_points.binary_search(&time) { + Ok(idx) => values.get(idx), + Err(_) => None, + } } } } -} -#[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 + /// 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 where - Self::Value: Copy + LinearInterpolatable, + T: Copy + LinearInterpolatable, { - Some(self.value) + 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) + } + } } - fn push(&mut self, _time: Duration, _value: Self::Value) -> ArgusResult { - Ok(false) + pub fn time_points(&self) -> Option> { + match self { + Signal::Empty => None, + Signal::Constant { value: _ } => Vec::new().into(), + Signal::Sampled { values: _, time_points } => time_points.iter().collect_vec().into(), + } + } + + /// Return a list consisting of all the points where the two signals should be + /// sampled and synchronized for operations. + pub fn sync_points<'a>(&'a self, other: &'a Self) -> Option> { + use core::ops::Bound::*; + + if self.is_empty() || other.is_empty() { + return None; + } + match (self, other) { + (Signal::Empty, _) | (_, Signal::Empty) => None, + (Signal::Constant { value: _ }, Signal::Constant { value: _ }) => Vec::new().into(), + (Signal::Constant { value: _ }, Signal::Sampled { values: _, time_points }) + | (Signal::Sampled { values: _, time_points }, Signal::Constant { value: _ }) => { + time_points.iter().collect_vec().into() + } + ( + Signal::Sampled { + values: _, + time_points: lhs, + }, + Signal::Sampled { + values: _, + time_points: rhs, + }, + ) => { + let bounds = match intersect_bounds(&self.bounds()?, &other.bounds()?) { + (Included(start), Included(end)) => start..=end, + (..) => unreachable!(), + }; + + itertools::merge(lhs, rhs) + .filter(|time| bounds.contains(time)) + .dedup() + .collect_vec() + .into() + } + } + } + + /// Augment synchronization points with time points where signals intersect + pub fn sync_with_intersection(&self, other: &Signal) -> Option> + where + T: PartialOrd + Copy + LinearInterpolatable + NumCast, + { + use core::cmp::Ordering::*; + let sync_points: Vec<&Duration> = self.sync_points(other)?.into_iter().collect(); + // This will contain the new signal with an initial capacity of twice the input + // signals sample points (as that is the upper limit of the number of new points + // that will be added + let mut return_points = Vec::::with_capacity(sync_points.len() * 2); + // this will contain the last sample point and ordering + let mut last_sample = None; + // We will now loop over the sync points, compare across signals and (if + // an intersection happens) we will have to compute the intersection point + for t in sync_points { + let lhs = self.at(*t).expect("value must be present at given time"); + let rhs = other.at(*t).expect("values must be present at given time"); + let ord = lhs.partial_cmp(rhs).unwrap(); + + // We will check for any intersections between the current sample and the + // previous one before we push the current sample time + if let Some((tm1, last)) = last_sample { + // Check if the signals crossed, this will happen essentially if the last + // and the current are opposites and were not Equal. + if let (Less, Greater) | (Greater, Less) = (last, ord) { + // Find the point of intersection between the points. + let a = utils::Neighborhood { + first: self.at(tm1).copied().map(|value| Sample { time: tm1, value }), + second: self.at(*t).copied().map(|value| Sample { time: *t, value }), + }; + let b = utils::Neighborhood { + first: other.at(tm1).copied().map(|value| Sample { time: tm1, value }), + second: other.at(*t).copied().map(|value| Sample { time: *t, value }), + }; + let intersect = utils::find_intersection(&a, &b); + return_points.push(intersect.time); + } + } + return_points.push(*t); + last_sample = Some((*t, ord)); + } + return_points.shrink_to_fit(); + Some(return_points) } } @@ -322,45 +447,19 @@ pub mod arbitrary { } /// Generate an arbitrary constant signal - pub fn constant_signal() -> impl Strategy> + pub fn constant_signal() -> impl Strategy> where - T: Arbitrary, + T: Arbitrary + Copy, { - any::().prop_map(ConstantSignal::new) - } - - /// Generate an arbitrary boolean signal - pub fn any_bool_signal(size: impl Into) -> impl Strategy { - prop_oneof![ - constant_signal::().prop_map(AnySignal::from), - sampled_signal::(size).prop_map(AnySignal::from), - ] - } - - /// Generate an arbitrary numeric signal - pub fn any_num_signal(size: impl Into + Clone) -> impl Strategy { - prop_oneof![ - constant_signal::().prop_map(AnySignal::from), - constant_signal::().prop_map(AnySignal::from), - constant_signal::().prop_map(AnySignal::from), - sampled_signal::(size.clone()).prop_map(AnySignal::from), - sampled_signal::(size.clone()).prop_map(AnySignal::from), - sampled_signal::(size).prop_map(AnySignal::from), - ] + any::().prop_map(Signal::constant) } /// Generate an arbitrary signal - pub fn any_signal(size: impl Into + Clone) -> impl Strategy { - prop_oneof![ - constant_signal::().prop_map(AnySignal::from), - constant_signal::().prop_map(AnySignal::from), - constant_signal::().prop_map(AnySignal::from), - constant_signal::().prop_map(AnySignal::from), - sampled_signal::(size.clone()).prop_map(AnySignal::from), - sampled_signal::(size.clone()).prop_map(AnySignal::from), - sampled_signal::(size.clone()).prop_map(AnySignal::from), - sampled_signal::(size).prop_map(AnySignal::from), - ] + pub fn signal(size: impl Into) -> impl Strategy> + where + T: Arbitrary + Copy, + { + prop_oneof![constant_signal::(), sampled_signal::(size),] } } @@ -387,8 +486,8 @@ mod tests { // 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.start_time(), Some(Bound::Included(start_time))); + assert_eq!(signal.end_time(), Some(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); @@ -500,10 +599,10 @@ mod tests { 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); + match (sig1, sig2, new_sig) { + (Signal::Constant { value: v1 }, Signal::Constant { value: v2 }, Signal::Constant { value: v }) => assert_eq!(v1 $op v2, v), + (s1, s2, s3) => panic!("{:?}, {:?} = {:?}", s1, s2, s3), + } } } }; diff --git a/argus-core/src/signals/bool_ops.rs b/argus-core/src/signals/bool_ops.rs index 6ca399e..71c52ca 100644 --- a/argus-core/src/signals/bool_ops.rs +++ b/argus-core/src/signals/bool_ops.rs @@ -1,5 +1,5 @@ -use crate::signals::utils::{apply1, apply2, apply2_const}; -use crate::signals::{ConstantSignal, Signal}; +use crate::signals::utils::{apply1, apply2}; +use crate::signals::Signal; impl core::ops::Not for &Signal { type Output = Signal; @@ -17,14 +17,6 @@ impl core::ops::BitAnd for &Signal { } } -impl core::ops::BitAnd<&ConstantSignal> for &Signal { - type Output = Signal; - - fn bitand(self, other: &ConstantSignal) -> Self::Output { - apply2_const(self, other, |lhs, rhs| lhs && rhs) - } -} - impl core::ops::BitOr for &Signal { type Output = Signal; @@ -32,50 +24,3 @@ impl core::ops::BitOr for &Signal { apply2(self, other, |lhs, rhs| lhs || rhs) } } - -impl core::ops::BitOr<&ConstantSignal> for &Signal { - type Output = Signal; - - fn bitor(self, other: &ConstantSignal) -> Self::Output { - apply2_const(self, other, |lhs, rhs| lhs || rhs) - } -} - -impl core::ops::Not for &ConstantSignal { - type Output = ConstantSignal; - - fn not(self) -> Self::Output { - ConstantSignal::::new(!self.value) - } -} - -impl core::ops::BitAnd for &ConstantSignal { - type Output = ConstantSignal; - - fn bitand(self, rhs: Self) -> Self::Output { - ConstantSignal::::new(self.value && rhs.value) - } -} - -impl core::ops::BitAnd<&Signal> for &ConstantSignal { - type Output = Signal; - - fn bitand(self, rhs: &Signal) -> Self::Output { - rhs & self - } -} -impl core::ops::BitOr for &ConstantSignal { - type Output = ConstantSignal; - - fn bitor(self, rhs: Self) -> Self::Output { - ConstantSignal::::new(self.value || rhs.value) - } -} - -impl core::ops::BitOr<&Signal> for &ConstantSignal { - type Output = Signal; - - fn bitor(self, rhs: &Signal) -> Self::Output { - rhs | self - } -} diff --git a/argus-core/src/signals/cast.rs b/argus-core/src/signals/cast.rs index ca367c3..7a75b7b 100644 --- a/argus-core/src/signals/cast.rs +++ b/argus-core/src/signals/cast.rs @@ -1,80 +1,71 @@ -use itertools::Itertools; -use num_traits::{Num, NumCast}; +use core::iter::zip; use crate::signals::traits::SignalNumCast; -use crate::signals::{ConstantSignal, Signal}; +use crate::signals::Signal; macro_rules! impl_cast { - ($type:ty) => { + (bool => $to:ty) => { paste::paste! { #[inline] - fn [](&self) -> Option> { - let samples = self - .iter() - .map_while(|(&t, &v)| num_traits::cast::<_, $type>(v).map(|v| (t, v))) - .collect_vec(); - if samples.len() < self.time_points.len() { - // Failed to convert some item - None - } else { - Some(samples.into_iter().collect()) + fn [](&self) -> Option> { + match self { + Signal::Empty => Some(Signal::Empty), + Signal::Constant { value } => num_traits::cast::<_, $to>(*value as i64).map(Signal::constant), + Signal::Sampled { values, time_points } => { + zip(time_points, values) + .map(|(&t, &v)| { + let val = num_traits::cast::<_, $to>(v as i64)?; + Some((t, val)) + }) + .collect() + } } } } }; -} - -impl SignalNumCast for Signal -where - T: Num + NumCast + Copy, -{ - type Value = T; - - type Output = Signal - where - U: Num + NumCast + Copy; - - impl_cast!(i8); - impl_cast!(i16); - impl_cast!(i32); - impl_cast!(i64); - impl_cast!(u8); - impl_cast!(u16); - impl_cast!(u32); - impl_cast!(u64); - impl_cast!(f32); - impl_cast!(f64); -} - -macro_rules! impl_cast { - ($type:ty) => { + ($from:ty => $to:ty) => { paste::paste! { #[inline] - fn [](&self) -> Option> { - num_traits::cast::<_, $type>(self.value).map(ConstantSignal::new) + fn [](&self) -> Option> { + match self { + Signal::Empty => Some(Signal::Empty), + Signal::Constant { value } => num_traits::cast::<_, $to>(*value).map(Signal::constant), + Signal::Sampled { values, time_points } => { + zip(time_points, values) + .map(|(&t, &v)| { + let val = num_traits::cast::<_, $to>(v)?; + Some((t, val)) + }) + .collect() + } + } } } }; + + ($from:ty) => { + impl SignalNumCast for Signal<$from> { + impl_cast!($from => i8); + impl_cast!($from => i16); + impl_cast!($from => i32); + impl_cast!($from => i64); + impl_cast!($from => u8); + impl_cast!($from => u16); + impl_cast!($from => u32); + impl_cast!($from => u64); + impl_cast!($from => f32); + impl_cast!($from => f64); + } + }; } -impl SignalNumCast for ConstantSignal -where - T: Num + NumCast + Copy, -{ - type Value = T; - - type Output = ConstantSignal - where - U: Num + NumCast + Copy; - - impl_cast!(i8); - impl_cast!(i16); - impl_cast!(i32); - impl_cast!(i64); - impl_cast!(u8); - impl_cast!(u16); - impl_cast!(u32); - impl_cast!(u64); - impl_cast!(f32); - impl_cast!(f64); -} +impl_cast!(i8); +impl_cast!(i16); +impl_cast!(i32); +impl_cast!(i64); +impl_cast!(u8); +impl_cast!(u16); +impl_cast!(u32); +impl_cast!(u64); +impl_cast!(f32); +impl_cast!(f64); diff --git a/argus-core/src/signals/cmp_ops.rs b/argus-core/src/signals/cmp_ops.rs index 55c86a7..5b79853 100644 --- a/argus-core/src/signals/cmp_ops.rs +++ b/argus-core/src/signals/cmp_ops.rs @@ -2,17 +2,14 @@ use std::cmp::Ordering; use num_traits::NumCast; -use super::traits::{BaseSignal, LinearInterpolatable, SignalMinMax, SignalPartialOrd, SignalSyncPoints}; -use super::utils::sync_with_intersection; -use super::{ConstantSignal, InterpolationMethod, Signal}; +use super::traits::{LinearInterpolatable, SignalMinMax, SignalPartialOrd}; +use super::{InterpolationMethod, Signal}; impl SignalPartialOrd for Signal where T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable, { - type Output = Signal; - - fn signal_cmp(&self, other: &Self, op: F) -> Option + fn signal_cmp(&self, other: &Self, op: F) -> Option> where F: Fn(Ordering) -> bool, { @@ -22,7 +19,7 @@ where // an intermediate point where the two signals are equal. This point must be // added to the signal appropriately. // the union of the sample points in self and other - let sync_points = sync_with_intersection(self, other)?; + let sync_points = self.sync_with_intersection(other)?; let sig: Signal = sync_points .into_iter() .map(|t| { @@ -35,90 +32,14 @@ where } } -impl SignalPartialOrd> for Signal +impl SignalMinMax for Signal where - T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable, -{ - type Output = Signal; - - fn signal_cmp(&self, other: &ConstantSignal, op: F) -> Option - where - F: Fn(Ordering) -> bool, - { - use super::InterpolationMethod::Linear; - // This has to be manually implemented and cannot use the apply2 functions. - // This is because if we have two signals that cross each other, then there is - // an intermediate point where the two signals are equal. This point must be - // added to the signal appropriately. - // the union of the sample points in self and other - let sync_points = sync_with_intersection(self, other)?; - let sig: Signal = sync_points - .into_iter() - .map(|t| { - let lhs = self.interpolate_at(t, Linear).unwrap(); - let rhs = other.interpolate_at(t, Linear).unwrap(); - (t, op(lhs.partial_cmp(&rhs).unwrap())) - }) - .collect(); - Some(sig) - } -} - -impl SignalPartialOrd> for ConstantSignal -where - T: PartialOrd + Copy + std::fmt::Debug + NumCast, -{ - type Output = ConstantSignal; - - fn signal_cmp(&self, other: &ConstantSignal, op: F) -> Option - where - F: Fn(Ordering) -> bool, - { - self.value.partial_cmp(&other.value).map(op).map(ConstantSignal::new) - } -} - -impl SignalPartialOrd> for ConstantSignal -where - T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable, -{ - type Output = Signal; - - fn signal_cmp(&self, other: &Signal, op: F) -> Option - where - F: Fn(Ordering) -> bool, - { - other.signal_cmp(self, op) - } -} - -impl SignalMinMax for ConstantSignal -where - T: PartialOrd + Copy, -{ - type Output = ConstantSignal; - - fn min(&self, rhs: &Self) -> Self::Output { - let value = if self.value < rhs.value { self.value } else { rhs.value }; - ConstantSignal::new(value) - } - - fn max(&self, rhs: &Self) -> Self::Output { - let value = if self.value > rhs.value { self.value } else { rhs.value }; - ConstantSignal::new(value) - } -} - -impl SignalMinMax for Lhs -where - T: PartialOrd + Copy + num_traits::NumCast + LinearInterpolatable, - Lhs: SignalSyncPoints + BaseSignal, - Rhs: SignalSyncPoints + BaseSignal, + T: PartialOrd + Copy + LinearInterpolatable + NumCast, { type Output = Signal; - fn min(&self, other: &Rhs) -> Self::Output { - let time_points = sync_with_intersection(self, other).unwrap(); + fn min(&self, other: &Self) -> Self::Output { + let time_points = self.sync_with_intersection(other).unwrap(); time_points .into_iter() .map(|t| { @@ -133,8 +54,8 @@ where .collect() } - fn max(&self, other: &Rhs) -> Self::Output { - let time_points = sync_with_intersection(self, other).unwrap(); + fn max(&self, other: &Self) -> Self::Output { + let time_points = self.sync_with_intersection(other).unwrap(); time_points .into_iter() .map(|t| { diff --git a/argus-core/src/signals/iter.rs b/argus-core/src/signals/iter.rs index af57f33..9bc7f82 100644 --- a/argus-core/src/signals/iter.rs +++ b/argus-core/src/signals/iter.rs @@ -1,17 +1,23 @@ -use std::iter::Zip; +use std::iter::{zip, Zip}; use std::time::Duration; use super::Signal; -pub struct Iter<'a, T> { - iter: Zip, core::slice::Iter<'a, T>>, +#[derive(Debug, Default)] +pub enum Iter<'a, T> { + #[default] + Empty, + Iter(Zip, core::slice::Iter<'a, T>>), } impl<'a, T> Iterator for Iter<'a, T> { type Item = (&'a Duration, &'a T); fn next(&mut self) -> Option { - self.iter.next() + match self { + Iter::Empty => None, + Iter::Iter(iter) => iter.next(), + } } } @@ -20,8 +26,10 @@ impl<'a, T> IntoIterator for &'a Signal { type Item = ::Item; fn into_iter(self) -> Self::IntoIter { - Iter { - iter: self.time_points.iter().zip(self.values.iter()), + match self { + Signal::Empty => Iter::default(), + Signal::Constant { value: _ } => Iter::default(), + Signal::Sampled { values, time_points } => Iter::Iter(zip(time_points, values)), } } } diff --git a/argus-core/src/signals/num_ops.rs b/argus-core/src/signals/num_ops.rs index 8d481ed..145e663 100644 --- a/argus-core/src/signals/num_ops.rs +++ b/argus-core/src/signals/num_ops.rs @@ -1,8 +1,8 @@ -use num_traits::{Num, NumCast, Signed}; +use num_traits::{NumCast, Signed}; -use super::traits::{BaseSignal, LinearInterpolatable, SignalAbs}; -use crate::signals::utils::{apply1, apply2, apply2_const, sync_with_intersection}; -use crate::signals::{ConstantSignal, Signal}; +use super::traits::{LinearInterpolatable, SignalAbs}; +use crate::signals::utils::{apply1, apply2}; +use crate::signals::Signal; impl core::ops::Neg for &Signal where @@ -16,21 +16,9 @@ where } } -impl core::ops::Neg for &ConstantSignal -where - T: Signed + Copy, -{ - type Output = ConstantSignal; - - /// Negate the signal at each time point - fn neg(self) -> Self::Output { - ConstantSignal::new(self.value.neg()) - } -} - impl core::ops::Add for &Signal where - T: core::ops::Add + Num + NumCast + Copy + LinearInterpolatable, + T: core::ops::Add + Copy + LinearInterpolatable, { type Output = Signal; @@ -40,45 +28,9 @@ where } } -impl core::ops::Add for &ConstantSignal -where - T: core::ops::Add + Num + Copy, -{ - type Output = ConstantSignal; - - /// Add the given signal with another - fn add(self, rhs: Self) -> Self::Output { - ConstantSignal::::new(self.value + rhs.value) - } -} - -impl core::ops::Add<&ConstantSignal> for &Signal -where - T: core::ops::Add + Num + NumCast + Copy + LinearInterpolatable, -{ - type Output = Signal; - - /// Add the given signal with another - fn add(self, rhs: &ConstantSignal) -> Self::Output { - apply2_const(self, rhs, |lhs, rhs| lhs + rhs) - } -} - -impl core::ops::Add<&Signal> for &ConstantSignal -where - T: core::ops::Add + Num + NumCast + Copy + LinearInterpolatable, -{ - type Output = Signal; - - /// Add the given signal with another - fn add(self, rhs: &Signal) -> Self::Output { - rhs + self - } -} - impl core::ops::Mul for &Signal where - T: core::ops::Mul + Num + NumCast + Copy + LinearInterpolatable, + T: core::ops::Mul + Copy + LinearInterpolatable, { type Output = Signal; @@ -88,46 +40,9 @@ where } } -impl core::ops::Mul for &ConstantSignal -where - T: core::ops::Mul + Num + Copy, -{ - type Output = ConstantSignal; - - /// Multiply the given signal with another - fn mul(self, rhs: Self) -> Self::Output { - ConstantSignal::::new(self.value * rhs.value) - } -} - -impl core::ops::Mul<&ConstantSignal> for &Signal -where - T: core::ops::Mul + Num + NumCast + Copy + LinearInterpolatable, -{ - type Output = Signal; - - /// Multiply the given signal with another - fn mul(self, rhs: &ConstantSignal) -> Self::Output { - apply2_const(self, rhs, |lhs, rhs| lhs * rhs) - } -} - -impl core::ops::Mul<&Signal> for &ConstantSignal -where - T: core::ops::Mul + Num + NumCast + Copy + LinearInterpolatable, -{ - type Output = Signal; - - /// Multiply the given signal with another - fn mul(self, rhs: &Signal) -> Self::Output { - rhs * self - } -} - impl core::ops::Sub for &Signal where - T: core::ops::Sub + Num + NumCast + Copy + LinearInterpolatable + PartialOrd, - Signal: BaseSignal, + T: core::ops::Sub + Copy + LinearInterpolatable + PartialOrd + NumCast, { type Output = Signal; @@ -145,73 +60,7 @@ where } // the union of the sample points in self and other - let sync_points = sync_with_intersection(self, rhs).unwrap(); - sync_points - .into_iter() - .map(|t| { - let lhs = self.interpolate_at(t, Linear).unwrap(); - let rhs = rhs.interpolate_at(t, Linear).unwrap(); - (t, lhs - rhs) - }) - .collect() - } -} - -impl core::ops::Sub for &ConstantSignal -where - T: core::ops::Sub + Num + Copy, -{ - type Output = ConstantSignal; - - /// Subtract the given signal with another - fn sub(self, rhs: Self) -> Self::Output { - ConstantSignal::::new(self.value - rhs.value) - } -} - -impl core::ops::Sub<&ConstantSignal> for &Signal -where - T: core::ops::Sub + Num + NumCast + Copy + LinearInterpolatable + PartialOrd, - Signal: BaseSignal, - ConstantSignal: BaseSignal, -{ - type Output = Signal; - - /// Subtract the given signal with another - fn sub(self, rhs: &ConstantSignal) -> Self::Output { - use super::InterpolationMethod::Linear; - // This has to be manually implemented and cannot use the apply2 functions. - // This is because if we have two signals that cross each other, then there is - // an intermediate point where the two signals are equal. This point must be - // added to the signal appropriately. - // the union of the sample points in self and other - let sync_points = sync_with_intersection(self, rhs).unwrap(); - sync_points - .into_iter() - .map(|t| { - let lhs = self.interpolate_at(t, Linear).unwrap(); - let rhs = rhs.interpolate_at(t, Linear).unwrap(); - (t, lhs - rhs) - }) - .collect() - } -} - -impl core::ops::Sub<&Signal> for &ConstantSignal -where - T: core::ops::Sub + Num + NumCast + Copy + LinearInterpolatable + PartialOrd, -{ - type Output = Signal; - - /// Subtract the given signal with another - fn sub(self, rhs: &Signal) -> Self::Output { - use super::InterpolationMethod::Linear; - // This has to be manually implemented and cannot use the apply2 functions. - // This is because if we have two signals that cross each other, then there is - // an intermediate point where the two signals are equal. This point must be - // added to the signal appropriately. - // the union of the sample points in self and other - let sync_points = sync_with_intersection(self, rhs).unwrap(); + let sync_points = self.sync_with_intersection(rhs).unwrap(); sync_points .into_iter() .map(|t| { @@ -225,7 +74,7 @@ where impl core::ops::Div for &Signal where - T: core::ops::Div + Num + NumCast + Copy + LinearInterpolatable, + T: core::ops::Div + Copy + LinearInterpolatable, { type Output = Signal; @@ -235,45 +84,9 @@ where } } -impl core::ops::Div for &ConstantSignal -where - T: core::ops::Div + Num + Copy, -{ - type Output = ConstantSignal; - - /// Divide the given signal with another - fn div(self, rhs: Self) -> Self::Output { - ConstantSignal::::new(self.value / rhs.value) - } -} - -impl core::ops::Div<&ConstantSignal> for &Signal -where - T: core::ops::Div + Num + NumCast + Copy + LinearInterpolatable, -{ - type Output = Signal; - - /// Divide the given signal with another - fn div(self, rhs: &ConstantSignal) -> Self::Output { - apply2_const(self, rhs, |lhs, rhs| lhs / rhs) - } -} - -impl core::ops::Div<&Signal> for &ConstantSignal -where - T: core::ops::Div + Num + NumCast + Copy + LinearInterpolatable, -{ - type Output = Signal; - - /// Divide the given signal with another - fn div(self, rhs: &Signal) -> Self::Output { - apply2_const(rhs, self, |rhs, lhs| lhs / rhs) - } -} - impl num_traits::Pow for &Signal where - T: num_traits::Pow + Num + NumCast + Copy + LinearInterpolatable, + T: num_traits::Pow + Copy + LinearInterpolatable, { type Output = Signal; @@ -284,17 +97,6 @@ where } macro_rules! signal_abs_impl { - (const $( $ty:ty ), *) => { - $( - impl SignalAbs for ConstantSignal<$ty> { - /// Return the absolute value for the signal - fn abs(&self) -> ConstantSignal<$ty> { - ConstantSignal::new(self.value.abs()) - } - } - )* - }; - ($( $ty:ty ), *) => { $( impl SignalAbs for Signal<$ty> { @@ -315,12 +117,3 @@ impl SignalAbs for Signal { apply1(self, |v| v) } } - -signal_abs_impl!(const i64, f32, f64); - -impl SignalAbs for ConstantSignal { - /// Return the absolute value for the signal - fn abs(&self) -> ConstantSignal { - ConstantSignal::new(self.value) - } -} diff --git a/argus-core/src/signals/traits.rs b/argus-core/src/signals/traits.rs index 50c1b13..485a787 100644 --- a/argus-core/src/signals/traits.rs +++ b/argus-core/src/signals/traits.rs @@ -1,98 +1,9 @@ use std::cmp::Ordering; -use std::iter::Empty; -use std::ops::RangeBounds; use std::time::Duration; -use itertools::Itertools; -use num_traits::{Num, NumCast}; use paste::paste; -use super::{ConstantSignal, InterpolationMethod, Sample, Signal}; -use crate::signals::utils::intersect_bounds; -use crate::ArgusResult; - -/// A general Signal trait -pub trait BaseSignal { - /// Type of the values contained in the signal. - /// - /// For example, a signal that implements `BaseSignal` contains a - /// sequence of timestamped `f64` values. - type Value; - - /// A type that implements [`RangeBounds`] to determine the duration bounds of the - /// signal. - /// - /// In practice, this should only be either [`RangeFull`](core::ops::RangeFull) - /// (returned by constant signals) or [`Range`](core::ops::Range) (returned by - /// sampled signals). - type Bounds: RangeBounds; - - /// Get 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. - /// Otherwise, `None` is returned. If the goal is to interpolate the value at the - /// a given time, see [`interpolate_at`](Self::interpolate_at). - fn at(&self, time: Duration) -> Option<&Self::Value>; - - /// 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. - fn interpolate_at(&self, time: Duration, interp: InterpolationMethod) -> Option - where - Self::Value: Copy + LinearInterpolatable; - - /// Get the bounds for the signal - fn bounds(&self) -> Self::Bounds; - - /// Push a new sample to the signal at the given time point - /// - /// The method should enforce the invariant that the time points of the signal must - /// have strictly monotonic increasing values, otherwise it returns an error without - /// adding the sample point. - /// - /// The result contains `true` if the sample was successfully added. For example, - /// pusing a value to a [constant signal](crate::signals::constant) will be a no-op - /// and return `false`. - fn push(&mut self, time: Duration, value: Self::Value) -> ArgusResult; - - /// Check if the signal is empty - fn is_empty(&self) -> bool { - use core::ops::Bound::*; - let bounds = self.bounds(); - match (bounds.start_bound(), bounds.end_bound()) { - (Included(start), Included(end)) => start > end, - (Included(start), Excluded(end)) | (Excluded(start), Included(end)) | (Excluded(start), Excluded(end)) => { - start >= end - } - - (Unbounded, Unbounded) => false, - bound => unreachable!("Argus doesn't support signals with bound {:?}", bound), - } - } - - /// Get the time at which the given signal starts. - fn start_time(&self) -> core::ops::Bound { - self.bounds().start_bound().cloned() - } - - /// Get the time at which the given signal ends. - fn end_time(&self) -> core::ops::Bound { - self.bounds().end_bound().cloned() - } -} - -/// A Boolean signal -pub trait BaseBooleanSignal: BaseSignal {} - -/// A numeric signal -pub trait BaseNumericSignal: BaseSignal { - type Value: Num; -} +use super::{Sample, Signal}; /// Trait for values that are linear interpolatable pub trait LinearInterpolatable { @@ -162,144 +73,10 @@ interpolate_for_num!(u64); interpolate_for_num!(f32); interpolate_for_num!(f64); -pub trait SignalSamplePoints { - type Output<'a>: IntoIterator - where - Self: 'a; - - /// Get the time points where the signal is sampled. - fn time_points(&'_ self) -> Option>; -} - -pub trait SignalSyncPoints { - type Output<'a>: IntoIterator - where - Self: 'a, - Rhs: 'a; - - /// Return the union list of time points where each of the given signals is sampled. - fn synchronization_points<'a>(&'a self, other: &'a Rhs) -> Option>; -} - -impl SignalSamplePoints for Signal -where - Signal: BaseSignal, - T: Copy, -{ - type Output<'a> = Vec<&'a Duration> - where - Self: 'a; - - fn time_points(&'_ self) -> Option> { - if self.is_empty() { - None - } else { - self.time_points.iter().collect_vec().into() - } - } -} - -impl SignalSamplePoints for ConstantSignal -where - T: Copy, -{ - type Output<'a> = Empty<&'a Duration> - where - Self: 'a; - - fn time_points(&'_ self) -> Option> { - if self.is_empty() { - None - } else { - core::iter::empty().into() - } - } -} - -impl SignalSyncPoints for Signal -where - T: Copy, - Self: BaseSignal, -{ - type Output<'a> = Vec<&'a Duration> - where - Self: 'a, - Self: 'a; - - fn synchronization_points<'a>(&'a self, other: &'a Self) -> Option> { - use core::ops::Bound::*; - if self.is_empty() || other.is_empty() { - return None; - } - - let bounds = match intersect_bounds(&self.bounds(), &other.bounds()) { - (Included(start), Included(end)) => start..=end, - (..) => unreachable!(), - }; - - self.time_points - .iter() - .merge(other.time_points.iter()) - .filter(|time| bounds.contains(time)) - .dedup() - .collect_vec() - .into() - } -} - -impl SignalSyncPoints> for Signal -where - T: Copy, - Self: BaseSignal, -{ - type Output<'a> = Vec<&'a Duration> - where - Self: 'a, - Self: 'a; - - fn synchronization_points<'a>(&'a self, other: &'a ConstantSignal) -> Option> { - if self.is_empty() || other.is_empty() { - return None; - } - - self.time_points.iter().collect_vec().into() - } -} - -// impl SignalSyncPoints> for ConstantSignal -// where -// T: Copy, -// Self: BaseSignal, -// { -// type Output<'a> = Empty<&'a Duration> -// where -// Self: 'a, -// Self: 'a; -// -// fn synchronization_points<'a>(&'a self, _other: &'a ConstantSignal) -> -// Option> { Some(core::iter::empty()) -// } -// } - -impl SignalSyncPoints> for ConstantSignal -where - T: Copy, - Self: BaseSignal, -{ - type Output<'a> = Vec<&'a Duration> - where - Self: 'a, - Self: 'a; - - fn synchronization_points<'a>(&'a self, other: &'a Signal) -> Option> { - other.synchronization_points(self) - } -} - macro_rules! impl_signal_cmp { ($cmp:ident) => { paste! { - fn [](&self, other: &Rhs) -> Option { + fn [](&self, other: &Rhs) -> Option> { self.signal_cmp(other, |ord| ord.[]()) } } @@ -307,15 +84,13 @@ macro_rules! impl_signal_cmp { } /// A time-wise partial ordering defined for signals -pub trait SignalPartialOrd: BaseSignal { - type Output: BaseSignal; - +pub trait SignalPartialOrd { /// Compare two signals within each of their domains (using [`PartialOrd`]) and /// apply the given function `op` to the ordering to create a signal. /// /// This function returns `None` if the comparison isn't possible, namely, when /// either of the signals are empty. - fn signal_cmp(&self, other: &Rhs, op: F) -> Option + fn signal_cmp(&self, other: &Rhs, op: F) -> Option> where F: Fn(Ordering) -> bool; @@ -328,8 +103,8 @@ pub trait SignalPartialOrd: BaseSignal { } /// Time-wise min-max of signal types -pub trait SignalMinMax: BaseSignal { - type Output: BaseSignal; +pub trait SignalMinMax { + type Output; /// Compute the time-wise min of two signals fn min(&self, rhs: &Rhs) -> Self::Output; @@ -340,21 +115,16 @@ pub trait SignalMinMax: BaseSignal { /// Trait for converting between numeric signal types pub trait SignalNumCast { - type Value: Num + NumCast; - type Output: BaseSignal - where - T: Num + NumCast + Copy; - - fn to_i8(&self) -> Option>; - fn to_i16(&self) -> Option>; - fn to_i32(&self) -> Option>; - fn to_i64(&self) -> Option>; - fn to_u8(&self) -> Option>; - fn to_u16(&self) -> Option>; - fn to_u32(&self) -> Option>; - fn to_u64(&self) -> Option>; - fn to_f32(&self) -> Option>; - fn to_f64(&self) -> Option>; + fn to_i8(&self) -> Option>; + fn to_i16(&self) -> Option>; + fn to_i32(&self) -> Option>; + fn to_i64(&self) -> Option>; + fn to_u8(&self) -> Option>; + fn to_u16(&self) -> Option>; + fn to_u32(&self) -> Option>; + fn to_u64(&self) -> Option>; + fn to_f32(&self) -> Option>; + fn to_f64(&self) -> Option>; } /// Trait for computing the absolute value of the samples in a signal diff --git a/argus-core/src/signals/utils.rs b/argus-core/src/signals/utils.rs index 7c3da7a..1e0c878 100644 --- a/argus-core/src/signals/utils.rs +++ b/argus-core/src/signals/utils.rs @@ -6,12 +6,12 @@ use core::ops::{Bound, RangeBounds}; use core::time::Duration; -use std::cmp::Ordering; +use std::iter::zip; use num_traits::NumCast; -use super::traits::{LinearInterpolatable, SignalSyncPoints}; -use super::{BaseSignal, ConstantSignal, InterpolationMethod, Sample, Signal}; +use super::traits::LinearInterpolatable; +use super::{InterpolationMethod, Sample, Signal}; /// The neighborhood around a signal such that the time `at` is between the `first` and /// `second` samples. @@ -61,108 +61,55 @@ where Sample { time: t, value: y } } -/// Augment synchronization points with time points where signals intersect -pub fn sync_with_intersection<'a, T, Sig1, Sig2>(sig1: &'a Sig1, sig2: &'a Sig2) -> Option> -where - T: PartialOrd + Copy + NumCast + LinearInterpolatable, - Sig1: BaseSignal + SignalSyncPoints, - Sig2: BaseSignal + SignalSyncPoints, -{ - use Ordering::*; - let sync_points: Vec<&Duration> = sig1.synchronization_points(sig2)?.into_iter().collect(); - // This will contain the new signal with an initial capacity of twice the input - // signals sample points (as that is the upper limit of the number of new points - // that will be added - let mut return_points = Vec::::with_capacity(sync_points.len() * 2); - // this will contain the last sample point and ordering - let mut last_sample = None; - // We will now loop over the sync points, compare across signals and (if - // an intersection happens) we will have to compute the intersection point - for t in sync_points { - let lhs = sig1.at(*t).expect("value must be present at given time"); - let rhs = sig2.at(*t).expect("values must be present at given time"); - let ord = lhs.partial_cmp(rhs).unwrap(); - - // We will check for any intersections between the current sample and the - // previous one before we push the current sample time - if let Some((tm1, last)) = last_sample { - // Check if the signals crossed, this will happen essentially if the last - // and the current are opposites and were not Equal. - if let (Less, Greater) | (Greater, Less) = (last, ord) { - // Find the point of intersection between the points. - let a = Neighborhood { - first: sig1.at(tm1).copied().map(|value| Sample { time: tm1, value }), - second: sig1.at(*t).copied().map(|value| Sample { time: *t, value }), - }; - let b = Neighborhood { - first: sig2.at(tm1).copied().map(|value| Sample { time: tm1, value }), - second: sig2.at(*t).copied().map(|value| Sample { time: *t, value }), - }; - let intersect = find_intersection(&a, &b); - return_points.push(intersect.time); - } - } - return_points.push(*t); - last_sample = Some((*t, ord)); - } - return_points.shrink_to_fit(); - Some(return_points) -} - +#[inline] pub fn apply1(signal: &Signal, op: F) -> Signal where T: Copy, F: Fn(T) -> U, Signal: std::iter::FromIterator<(Duration, U)>, { - signal.iter().map(|(t, v)| (*t, op(*v))).collect() + match signal { + Signal::Empty => Signal::Empty, + Signal::Constant { value } => Signal::Constant { value: op(*value) }, + Signal::Sampled { values, time_points } => { + zip(time_points.iter().copied(), values.iter().map(|v| op(*v))).collect() + } + } } +#[inline] pub fn apply2<'a, T, U, F>(lhs: &'a Signal, rhs: &'a Signal, op: F) -> Signal where T: Copy + LinearInterpolatable, U: Copy, F: Fn(T, T) -> U, { + use Signal::*; // If either of the signals are empty, we return an empty signal. if lhs.is_empty() || rhs.is_empty() { // Intersection with empty signal should yield an empty signal return Signal::::new(); } - // We determine the range of the signal (as the output signal can only be - // defined in the domain where both signals are defined). - let time_points = lhs.synchronization_points(rhs).unwrap(); - // Now, at each of the merged time points, we sample each signal and operate on - // them - time_points - .into_iter() - .map(|t| { - let v1 = lhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap(); - let v2 = rhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap(); - (*t, op(v1, v2)) - }) - .collect() -} - -pub fn apply2_const<'a, T, U, F>(lhs: &'a Signal, rhs: &'a ConstantSignal, op: F) -> Signal -where - T: Copy + LinearInterpolatable, - U: Copy, - F: Fn(T, T) -> U, -{ - // If either of the signals are empty, we return an empty signal. - if lhs.is_empty() { - // Intersection with empty signal should yield an empty signal - return Signal::::new(); + match (lhs, rhs) { + // If either of the signals are empty, we return an empty signal. + (Empty, _) | (_, Empty) => Signal::new(), + (Constant { value: v1 }, Constant { value: v2 }) => Signal::constant(op(*v1, *v2)), + (lhs, rhs) => { + // We determine the range of the signal (as the output signal can only be + // defined in the domain where both signals are defined). + let time_points = lhs.sync_points(rhs).unwrap(); + // Now, at each of the merged time points, we sample each signal and operate on + // them + time_points + .into_iter() + .map(|t| { + let v1 = lhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap(); + let v2 = rhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap(); + (*t, op(v1, v2)) + }) + .collect() + } } - lhs.time_points - .iter() - .map(|&t| { - let v1 = lhs.interpolate_at(t, InterpolationMethod::Linear).unwrap(); - let v2 = rhs.interpolate_at(t, InterpolationMethod::Linear).unwrap(); - (t, op(v1, v2)) - }) - .collect() } fn partial_min(a: T, b: T) -> Option