feat!(core): Change Signal to be a sumtype

We want to be able to reason about if a signal is empty, constant, or sampled
at compile time without using any trait objects. Moreover, the core Argus
library shouldn't care about how it deals with interfacing with other languages
like Python. Thus, we remove the need for having an `AnySignal` type and what
not.
This commit is contained in:
Anand Balakrishnan 2023-04-14 10:53:38 -07:00
parent a6a3805107
commit 4431b79bcd
No known key found for this signature in database
10 changed files with 442 additions and 966 deletions

View file

@ -13,6 +13,8 @@ pub enum Error {
#[error("insufficient number of arguments")] #[error("insufficient number of arguments")]
IncompleteArgs, IncompleteArgs,
#[error("cannot push value to non-sampled signal")]
InvalidPushToSignal,
#[error( #[error(
"trying to create a non-monotonically signal, signal end time ({end_time:?}) > sample time point \ "trying to create a non-monotonically signal, signal end time ({end_time:?}) > sample time point \
({current_sample:?})" ({current_sample:?})"

View file

@ -1,3 +1,3 @@
pub use crate::expr::{BoolExpr, Expr, ExprBuilder, ExprRef, NumExpr}; 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}; pub use crate::{ArgusError, ArgusResult};

View file

@ -16,30 +16,20 @@ pub mod num_ops;
pub mod traits; pub mod traits;
mod utils; mod utils;
use std::ops::{RangeFull, RangeInclusive}; use std::ops::{Bound, RangeBounds};
use std::time::Duration; use std::time::Duration;
pub use bool_ops::*; pub use bool_ops::*;
pub use cast::*; pub use cast::*;
pub use cmp_ops::*; pub use cmp_ops::*;
use itertools::Itertools;
pub use num_ops::*; 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}; use crate::{ArgusResult, Error};
/// All supported signal types in Argus
#[derive(Debug, Clone, derive_more::From)]
pub enum AnySignal {
Bool(Signal<bool>),
ConstBool(ConstantSignal<bool>),
Int(Signal<i64>),
ConstInt(ConstantSignal<i64>),
UInt(Signal<u64>),
ConstUInt(ConstantSignal<u64>),
Float(Signal<f64>),
ConstFloat(ConstantSignal<f64>),
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum InterpolationMethod { pub enum InterpolationMethod {
Linear, Linear,
@ -76,31 +66,116 @@ pub struct Sample<T> {
pub value: T, pub value: T,
} }
/// A signal is a sequence of time points ([`Duration`](core::time::Duration)) and /// A typed Signal
/// corresponding value samples. ///
#[derive(Default, Debug, Clone)] /// A Signal can either be empty, constant throughout its domain, or sampled at a
pub struct Signal<T> { /// finite set of strictly monotonically increasing time points.
pub(crate) values: Vec<T>, #[derive(Default, Clone, Debug)]
pub(crate) time_points: Vec<Duration>, pub enum Signal<T> {
#[default]
Empty,
Constant {
value: T,
},
Sampled {
values: Vec<T>,
time_points: Vec<Duration>,
},
} }
impl<T> Signal<T> { impl<T> Signal<T> {
/// Create a new empty signal /// Create a new empty signal
pub fn new() -> Self { pub fn new() -> Self {
Self { Self::Empty
values: Default::default(), }
time_points: Default::default(),
} /// Create a new constant signal
pub fn constant(value: T) -> Self {
Self::Constant { value }
} }
/// Create a new empty signal with the specified capacity /// Create a new empty signal with the specified capacity
pub fn new_with_capacity(size: usize) -> Self { pub fn new_with_capacity(size: usize) -> Self {
Self { Self::Sampled {
values: Vec::with_capacity(size), values: Vec::with_capacity(size),
time_points: 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<Duration>, Bound<Duration>)> {
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<Bound<Duration>> {
self.bounds().map(|b| b.0)
}
/// Get the time at which the given signal ends.
pub fn end_time(&self) -> Option<Bound<Duration>> {
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. /// Create an iterator over the pairs of time points and values of the signal.
pub fn iter(&self) -> impl Iterator<Item = (&Duration, &T)> { pub fn iter(&self) -> impl Iterator<Item = (&Duration, &T)> {
self.into_iter() self.into_iter()
@ -121,152 +196,202 @@ impl<T> Signal<T> {
} }
Ok(signal) Ok(signal)
} }
}
impl<T> BaseSignal for Signal<T> { /// Get the value of the signal at the given time point
type Value = T; ///
type Bounds = RangeInclusive<Duration>; /// 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> { // We will use binary search to find the appropriate index
assert_eq!( match time_points.binary_search(&time) {
self.time_points.len(), Ok(idx) => values.get(idx),
self.values.len(), Err(_) => None,
"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<Self::Value>
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<bool> {
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)] /// Interpolate the value of the signal at the given time point
pub struct ConstantSignal<T> { ///
pub value: T, /// 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
impl<T> ConstantSignal<T> { /// method cannot be used at the given time (for example, if we use
pub fn new(value: T) -> Self { /// [`InterpolationMethod::Linear`] and the `time` point is outside the signal
Self { value } /// domain), then a `None` is returned.
} pub fn interpolate_at(&self, time: Duration, interp: InterpolationMethod) -> Option<T>
}
impl<T> BaseSignal for ConstantSignal<T> {
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<Self::Value>
where 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<bool> { pub fn time_points(&self) -> Option<Vec<&Duration>> {
Ok(false) 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<Vec<&'a Duration>> {
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<T>) -> Option<Vec<Duration>>
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::<Duration>::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 /// Generate an arbitrary constant signal
pub fn constant_signal<T>() -> impl Strategy<Value = ConstantSignal<T>> pub fn constant_signal<T>() -> impl Strategy<Value = Signal<T>>
where where
T: Arbitrary, T: Arbitrary + Copy,
{ {
any::<T>().prop_map(ConstantSignal::new) any::<T>().prop_map(Signal::constant)
}
/// Generate an arbitrary boolean signal
pub fn any_bool_signal(size: impl Into<SizeRange>) -> impl Strategy<Value = AnySignal> {
prop_oneof![
constant_signal::<bool>().prop_map(AnySignal::from),
sampled_signal::<bool>(size).prop_map(AnySignal::from),
]
}
/// Generate an arbitrary numeric signal
pub fn any_num_signal(size: impl Into<SizeRange> + Clone) -> impl Strategy<Value = AnySignal> {
prop_oneof![
constant_signal::<i64>().prop_map(AnySignal::from),
constant_signal::<u64>().prop_map(AnySignal::from),
constant_signal::<f64>().prop_map(AnySignal::from),
sampled_signal::<i64>(size.clone()).prop_map(AnySignal::from),
sampled_signal::<u64>(size.clone()).prop_map(AnySignal::from),
sampled_signal::<f64>(size).prop_map(AnySignal::from),
]
} }
/// Generate an arbitrary signal /// Generate an arbitrary signal
pub fn any_signal(size: impl Into<SizeRange> + Clone) -> impl Strategy<Value = AnySignal> { pub fn signal<T>(size: impl Into<SizeRange>) -> impl Strategy<Value = Signal<T>>
prop_oneof![ where
constant_signal::<bool>().prop_map(AnySignal::from), T: Arbitrary + Copy,
constant_signal::<i64>().prop_map(AnySignal::from), {
constant_signal::<u64>().prop_map(AnySignal::from), prop_oneof![constant_signal::<T>(), sampled_signal::<T>(size),]
constant_signal::<f64>().prop_map(AnySignal::from),
sampled_signal::<bool>(size.clone()).prop_map(AnySignal::from),
sampled_signal::<i64>(size.clone()).prop_map(AnySignal::from),
sampled_signal::<u64>(size.clone()).prop_map(AnySignal::from),
sampled_signal::<f64>(size).prop_map(AnySignal::from),
]
} }
} }
@ -387,8 +486,8 @@ mod tests {
// Get the value of the sample at a given index // Get the value of the sample at a given index
let (at, val) = samples[idx]; let (at, val) = samples[idx];
assert_eq!(signal.start_time(), Bound::Included(start_time)); assert_eq!(signal.start_time(), Some(Bound::Included(start_time)));
assert_eq!(signal.end_time(), Bound::Included(end_time)); assert_eq!(signal.end_time(), Some(Bound::Included(end_time)));
assert_eq!(signal.at(at), Some(&val)); assert_eq!(signal.at(at), Some(&val));
assert_eq!(signal.at(end_time + Duration::from_secs(1)), None); assert_eq!(signal.at(end_time + Duration::from_secs(1)), None);
assert_eq!(signal.at(start_time - Duration::from_secs(1)), None); assert_eq!(signal.at(start_time - Duration::from_secs(1)), None);
@ -500,10 +599,10 @@ mod tests {
proptest! { proptest! {
|(sig1 in arbitrary::constant_signal::<$ty>(), sig2 in arbitrary::constant_signal::<$ty>())| { |(sig1 in arbitrary::constant_signal::<$ty>(), sig2 in arbitrary::constant_signal::<$ty>())| {
let new_sig = &sig1 $op &sig2; let new_sig = &sig1 $op &sig2;
let v1 = sig1.value; match (sig1, sig2, new_sig) {
let v2 = sig2.value; (Signal::Constant { value: v1 }, Signal::Constant { value: v2 }, Signal::Constant { value: v }) => assert_eq!(v1 $op v2, v),
let v = new_sig.value; (s1, s2, s3) => panic!("{:?}, {:?} = {:?}", s1, s2, s3),
assert_eq!(v1 $op v2, v); }
} }
} }
}; };

View file

@ -1,5 +1,5 @@
use crate::signals::utils::{apply1, apply2, apply2_const}; use crate::signals::utils::{apply1, apply2};
use crate::signals::{ConstantSignal, Signal}; use crate::signals::Signal;
impl core::ops::Not for &Signal<bool> { impl core::ops::Not for &Signal<bool> {
type Output = Signal<bool>; type Output = Signal<bool>;
@ -17,14 +17,6 @@ impl core::ops::BitAnd<Self> for &Signal<bool> {
} }
} }
impl core::ops::BitAnd<&ConstantSignal<bool>> for &Signal<bool> {
type Output = Signal<bool>;
fn bitand(self, other: &ConstantSignal<bool>) -> Self::Output {
apply2_const(self, other, |lhs, rhs| lhs && rhs)
}
}
impl core::ops::BitOr<Self> for &Signal<bool> { impl core::ops::BitOr<Self> for &Signal<bool> {
type Output = Signal<bool>; type Output = Signal<bool>;
@ -32,50 +24,3 @@ impl core::ops::BitOr<Self> for &Signal<bool> {
apply2(self, other, |lhs, rhs| lhs || rhs) apply2(self, other, |lhs, rhs| lhs || rhs)
} }
} }
impl core::ops::BitOr<&ConstantSignal<bool>> for &Signal<bool> {
type Output = Signal<bool>;
fn bitor(self, other: &ConstantSignal<bool>) -> Self::Output {
apply2_const(self, other, |lhs, rhs| lhs || rhs)
}
}
impl core::ops::Not for &ConstantSignal<bool> {
type Output = ConstantSignal<bool>;
fn not(self) -> Self::Output {
ConstantSignal::<bool>::new(!self.value)
}
}
impl core::ops::BitAnd<Self> for &ConstantSignal<bool> {
type Output = ConstantSignal<bool>;
fn bitand(self, rhs: Self) -> Self::Output {
ConstantSignal::<bool>::new(self.value && rhs.value)
}
}
impl core::ops::BitAnd<&Signal<bool>> for &ConstantSignal<bool> {
type Output = Signal<bool>;
fn bitand(self, rhs: &Signal<bool>) -> Self::Output {
rhs & self
}
}
impl core::ops::BitOr<Self> for &ConstantSignal<bool> {
type Output = ConstantSignal<bool>;
fn bitor(self, rhs: Self) -> Self::Output {
ConstantSignal::<bool>::new(self.value || rhs.value)
}
}
impl core::ops::BitOr<&Signal<bool>> for &ConstantSignal<bool> {
type Output = Signal<bool>;
fn bitor(self, rhs: &Signal<bool>) -> Self::Output {
rhs | self
}
}

View file

@ -1,80 +1,71 @@
use itertools::Itertools; use core::iter::zip;
use num_traits::{Num, NumCast};
use crate::signals::traits::SignalNumCast; use crate::signals::traits::SignalNumCast;
use crate::signals::{ConstantSignal, Signal}; use crate::signals::Signal;
macro_rules! impl_cast { macro_rules! impl_cast {
($type:ty) => { (bool => $to:ty) => {
paste::paste! { paste::paste! {
#[inline] #[inline]
fn [<to_ $type>](&self) -> Option<Signal<$type>> { fn [<to_ $to>](&self) -> Option<Signal<$to>> {
let samples = self match self {
.iter() Signal::Empty => Some(Signal::Empty),
.map_while(|(&t, &v)| num_traits::cast::<_, $type>(v).map(|v| (t, v))) Signal::Constant { value } => num_traits::cast::<_, $to>(*value as i64).map(Signal::constant),
.collect_vec(); Signal::Sampled { values, time_points } => {
if samples.len() < self.time_points.len() { zip(time_points, values)
// Failed to convert some item .map(|(&t, &v)| {
None let val = num_traits::cast::<_, $to>(v as i64)?;
} else { Some((t, val))
Some(samples.into_iter().collect()) })
.collect()
}
} }
} }
} }
}; };
} ($from:ty => $to:ty) => {
impl<T> SignalNumCast for Signal<T>
where
T: Num + NumCast + Copy,
{
type Value = T;
type Output<U> = Signal<U>
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) => {
paste::paste! { paste::paste! {
#[inline] #[inline]
fn [<to_ $type>](&self) -> Option<ConstantSignal<$type>> { fn [<to_ $to>](&self) -> Option<Signal<$to>> {
num_traits::cast::<_, $type>(self.value).map(ConstantSignal::new) 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<T> SignalNumCast for ConstantSignal<T> impl_cast!(i8);
where impl_cast!(i16);
T: Num + NumCast + Copy, impl_cast!(i32);
{ impl_cast!(i64);
type Value = T; impl_cast!(u8);
impl_cast!(u16);
type Output<U> = ConstantSignal<U> impl_cast!(u32);
where impl_cast!(u64);
U: Num + NumCast + Copy; 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);
}

View file

@ -2,17 +2,14 @@ use std::cmp::Ordering;
use num_traits::NumCast; use num_traits::NumCast;
use super::traits::{BaseSignal, LinearInterpolatable, SignalMinMax, SignalPartialOrd, SignalSyncPoints}; use super::traits::{LinearInterpolatable, SignalMinMax, SignalPartialOrd};
use super::utils::sync_with_intersection; use super::{InterpolationMethod, Signal};
use super::{ConstantSignal, InterpolationMethod, Signal};
impl<T> SignalPartialOrd<Self> for Signal<T> impl<T> SignalPartialOrd<Self> for Signal<T>
where where
T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable, T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable,
{ {
type Output = Signal<bool>; fn signal_cmp<F>(&self, other: &Self, op: F) -> Option<Signal<bool>>
fn signal_cmp<F>(&self, other: &Self, op: F) -> Option<Self::Output>
where where
F: Fn(Ordering) -> bool, F: Fn(Ordering) -> bool,
{ {
@ -22,7 +19,7 @@ where
// an intermediate point where the two signals are equal. This point must be // an intermediate point where the two signals are equal. This point must be
// added to the signal appropriately. // added to the signal appropriately.
// the union of the sample points in self and other // 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<bool> = sync_points let sig: Signal<bool> = sync_points
.into_iter() .into_iter()
.map(|t| { .map(|t| {
@ -35,90 +32,14 @@ where
} }
} }
impl<T> SignalPartialOrd<ConstantSignal<T>> for Signal<T> impl<T> SignalMinMax<Self> for Signal<T>
where where
T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable, T: PartialOrd + Copy + LinearInterpolatable + NumCast,
{
type Output = Signal<bool>;
fn signal_cmp<F>(&self, other: &ConstantSignal<T>, op: F) -> Option<Self::Output>
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<bool> = 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<T> SignalPartialOrd<ConstantSignal<T>> for ConstantSignal<T>
where
T: PartialOrd + Copy + std::fmt::Debug + NumCast,
{
type Output = ConstantSignal<bool>;
fn signal_cmp<F>(&self, other: &ConstantSignal<T>, op: F) -> Option<Self::Output>
where
F: Fn(Ordering) -> bool,
{
self.value.partial_cmp(&other.value).map(op).map(ConstantSignal::new)
}
}
impl<T> SignalPartialOrd<Signal<T>> for ConstantSignal<T>
where
T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable,
{
type Output = Signal<bool>;
fn signal_cmp<F>(&self, other: &Signal<T>, op: F) -> Option<Self::Output>
where
F: Fn(Ordering) -> bool,
{
other.signal_cmp(self, op)
}
}
impl<T> SignalMinMax for ConstantSignal<T>
where
T: PartialOrd + Copy,
{
type Output = ConstantSignal<T>;
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<T, Lhs, Rhs> SignalMinMax<Rhs> for Lhs
where
T: PartialOrd + Copy + num_traits::NumCast + LinearInterpolatable,
Lhs: SignalSyncPoints<Rhs> + BaseSignal<Value = T>,
Rhs: SignalSyncPoints<Self> + BaseSignal<Value = T>,
{ {
type Output = Signal<T>; type Output = Signal<T>;
fn min(&self, other: &Rhs) -> Self::Output { fn min(&self, other: &Self) -> Self::Output {
let time_points = sync_with_intersection(self, other).unwrap(); let time_points = self.sync_with_intersection(other).unwrap();
time_points time_points
.into_iter() .into_iter()
.map(|t| { .map(|t| {
@ -133,8 +54,8 @@ where
.collect() .collect()
} }
fn max(&self, other: &Rhs) -> Self::Output { fn max(&self, other: &Self) -> Self::Output {
let time_points = sync_with_intersection(self, other).unwrap(); let time_points = self.sync_with_intersection(other).unwrap();
time_points time_points
.into_iter() .into_iter()
.map(|t| { .map(|t| {

View file

@ -1,17 +1,23 @@
use std::iter::Zip; use std::iter::{zip, Zip};
use std::time::Duration; use std::time::Duration;
use super::Signal; use super::Signal;
pub struct Iter<'a, T> { #[derive(Debug, Default)]
iter: Zip<core::slice::Iter<'a, Duration>, core::slice::Iter<'a, T>>, pub enum Iter<'a, T> {
#[default]
Empty,
Iter(Zip<core::slice::Iter<'a, Duration>, core::slice::Iter<'a, T>>),
} }
impl<'a, T> Iterator for Iter<'a, T> { impl<'a, T> Iterator for Iter<'a, T> {
type Item = (&'a Duration, &'a T); type Item = (&'a Duration, &'a T);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.iter.next() match self {
Iter::Empty => None,
Iter::Iter(iter) => iter.next(),
}
} }
} }
@ -20,8 +26,10 @@ impl<'a, T> IntoIterator for &'a Signal<T> {
type Item = <Self::IntoIter as Iterator>::Item; type Item = <Self::IntoIter as Iterator>::Item;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
Iter { match self {
iter: self.time_points.iter().zip(self.values.iter()), Signal::Empty => Iter::default(),
Signal::Constant { value: _ } => Iter::default(),
Signal::Sampled { values, time_points } => Iter::Iter(zip(time_points, values)),
} }
} }
} }

View file

@ -1,8 +1,8 @@
use num_traits::{Num, NumCast, Signed}; use num_traits::{NumCast, Signed};
use super::traits::{BaseSignal, LinearInterpolatable, SignalAbs}; use super::traits::{LinearInterpolatable, SignalAbs};
use crate::signals::utils::{apply1, apply2, apply2_const, sync_with_intersection}; use crate::signals::utils::{apply1, apply2};
use crate::signals::{ConstantSignal, Signal}; use crate::signals::Signal;
impl<T> core::ops::Neg for &Signal<T> impl<T> core::ops::Neg for &Signal<T>
where where
@ -16,21 +16,9 @@ where
} }
} }
impl<T> core::ops::Neg for &ConstantSignal<T>
where
T: Signed + Copy,
{
type Output = ConstantSignal<T>;
/// Negate the signal at each time point
fn neg(self) -> Self::Output {
ConstantSignal::new(self.value.neg())
}
}
impl<T> core::ops::Add for &Signal<T> impl<T> core::ops::Add for &Signal<T>
where where
T: core::ops::Add<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable, T: core::ops::Add<T, Output = T> + Copy + LinearInterpolatable,
{ {
type Output = Signal<T>; type Output = Signal<T>;
@ -40,45 +28,9 @@ where
} }
} }
impl<T> core::ops::Add for &ConstantSignal<T>
where
T: core::ops::Add<T, Output = T> + Num + Copy,
{
type Output = ConstantSignal<T>;
/// Add the given signal with another
fn add(self, rhs: Self) -> Self::Output {
ConstantSignal::<T>::new(self.value + rhs.value)
}
}
impl<T> core::ops::Add<&ConstantSignal<T>> for &Signal<T>
where
T: core::ops::Add<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable,
{
type Output = Signal<T>;
/// Add the given signal with another
fn add(self, rhs: &ConstantSignal<T>) -> Self::Output {
apply2_const(self, rhs, |lhs, rhs| lhs + rhs)
}
}
impl<T> core::ops::Add<&Signal<T>> for &ConstantSignal<T>
where
T: core::ops::Add<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable,
{
type Output = Signal<T>;
/// Add the given signal with another
fn add(self, rhs: &Signal<T>) -> Self::Output {
rhs + self
}
}
impl<T> core::ops::Mul for &Signal<T> impl<T> core::ops::Mul for &Signal<T>
where where
T: core::ops::Mul<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable, T: core::ops::Mul<T, Output = T> + Copy + LinearInterpolatable,
{ {
type Output = Signal<T>; type Output = Signal<T>;
@ -88,46 +40,9 @@ where
} }
} }
impl<T> core::ops::Mul for &ConstantSignal<T>
where
T: core::ops::Mul<T, Output = T> + Num + Copy,
{
type Output = ConstantSignal<T>;
/// Multiply the given signal with another
fn mul(self, rhs: Self) -> Self::Output {
ConstantSignal::<T>::new(self.value * rhs.value)
}
}
impl<T> core::ops::Mul<&ConstantSignal<T>> for &Signal<T>
where
T: core::ops::Mul<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable,
{
type Output = Signal<T>;
/// Multiply the given signal with another
fn mul(self, rhs: &ConstantSignal<T>) -> Self::Output {
apply2_const(self, rhs, |lhs, rhs| lhs * rhs)
}
}
impl<T> core::ops::Mul<&Signal<T>> for &ConstantSignal<T>
where
T: core::ops::Mul<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable,
{
type Output = Signal<T>;
/// Multiply the given signal with another
fn mul(self, rhs: &Signal<T>) -> Self::Output {
rhs * self
}
}
impl<T> core::ops::Sub for &Signal<T> impl<T> core::ops::Sub for &Signal<T>
where where
T: core::ops::Sub<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable + PartialOrd, T: core::ops::Sub<T, Output = T> + Copy + LinearInterpolatable + PartialOrd + NumCast,
Signal<T>: BaseSignal<Value = T>,
{ {
type Output = Signal<T>; type Output = Signal<T>;
@ -145,73 +60,7 @@ where
} }
// the union of the sample points in self and other // 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| {
let lhs = self.interpolate_at(t, Linear).unwrap();
let rhs = rhs.interpolate_at(t, Linear).unwrap();
(t, lhs - rhs)
})
.collect()
}
}
impl<T> core::ops::Sub for &ConstantSignal<T>
where
T: core::ops::Sub<T, Output = T> + Num + Copy,
{
type Output = ConstantSignal<T>;
/// Subtract the given signal with another
fn sub(self, rhs: Self) -> Self::Output {
ConstantSignal::<T>::new(self.value - rhs.value)
}
}
impl<T> core::ops::Sub<&ConstantSignal<T>> for &Signal<T>
where
T: core::ops::Sub<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable + PartialOrd,
Signal<T>: BaseSignal<Value = T>,
ConstantSignal<T>: BaseSignal<Value = T>,
{
type Output = Signal<T>;
/// Subtract the given signal with another
fn sub(self, rhs: &ConstantSignal<T>) -> 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<T> core::ops::Sub<&Signal<T>> for &ConstantSignal<T>
where
T: core::ops::Sub<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable + PartialOrd,
{
type Output = Signal<T>;
/// Subtract the given signal with another
fn sub(self, rhs: &Signal<T>) -> 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 sync_points
.into_iter() .into_iter()
.map(|t| { .map(|t| {
@ -225,7 +74,7 @@ where
impl<T> core::ops::Div for &Signal<T> impl<T> core::ops::Div for &Signal<T>
where where
T: core::ops::Div<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable, T: core::ops::Div<T, Output = T> + Copy + LinearInterpolatable,
{ {
type Output = Signal<T>; type Output = Signal<T>;
@ -235,45 +84,9 @@ where
} }
} }
impl<T> core::ops::Div for &ConstantSignal<T>
where
T: core::ops::Div<T, Output = T> + Num + Copy,
{
type Output = ConstantSignal<T>;
/// Divide the given signal with another
fn div(self, rhs: Self) -> Self::Output {
ConstantSignal::<T>::new(self.value / rhs.value)
}
}
impl<T> core::ops::Div<&ConstantSignal<T>> for &Signal<T>
where
T: core::ops::Div<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable,
{
type Output = Signal<T>;
/// Divide the given signal with another
fn div(self, rhs: &ConstantSignal<T>) -> Self::Output {
apply2_const(self, rhs, |lhs, rhs| lhs / rhs)
}
}
impl<T> core::ops::Div<&Signal<T>> for &ConstantSignal<T>
where
T: core::ops::Div<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable,
{
type Output = Signal<T>;
/// Divide the given signal with another
fn div(self, rhs: &Signal<T>) -> Self::Output {
apply2_const(rhs, self, |rhs, lhs| lhs / rhs)
}
}
impl<T> num_traits::Pow<Self> for &Signal<T> impl<T> num_traits::Pow<Self> for &Signal<T>
where where
T: num_traits::Pow<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable, T: num_traits::Pow<T, Output = T> + Copy + LinearInterpolatable,
{ {
type Output = Signal<T>; type Output = Signal<T>;
@ -284,17 +97,6 @@ where
} }
macro_rules! signal_abs_impl { 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 ), *) => { ($( $ty:ty ), *) => {
$( $(
impl SignalAbs for Signal<$ty> { impl SignalAbs for Signal<$ty> {
@ -315,12 +117,3 @@ impl SignalAbs for Signal<u64> {
apply1(self, |v| v) apply1(self, |v| v)
} }
} }
signal_abs_impl!(const i64, f32, f64);
impl SignalAbs for ConstantSignal<u64> {
/// Return the absolute value for the signal
fn abs(&self) -> ConstantSignal<u64> {
ConstantSignal::new(self.value)
}
}

View file

@ -1,98 +1,9 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::iter::Empty;
use std::ops::RangeBounds;
use std::time::Duration; use std::time::Duration;
use itertools::Itertools;
use num_traits::{Num, NumCast};
use paste::paste; use paste::paste;
use super::{ConstantSignal, InterpolationMethod, Sample, Signal}; use super::{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<Value = f64, ...>` 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<Duration>;
/// 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<Self::Value>
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<bool>;
/// 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<Duration> {
self.bounds().start_bound().cloned()
}
/// Get the time at which the given signal ends.
fn end_time(&self) -> core::ops::Bound<Duration> {
self.bounds().end_bound().cloned()
}
}
/// A Boolean signal
pub trait BaseBooleanSignal: BaseSignal {}
/// A numeric signal
pub trait BaseNumericSignal: BaseSignal {
type Value: Num;
}
/// Trait for values that are linear interpolatable /// Trait for values that are linear interpolatable
pub trait LinearInterpolatable { pub trait LinearInterpolatable {
@ -162,144 +73,10 @@ interpolate_for_num!(u64);
interpolate_for_num!(f32); interpolate_for_num!(f32);
interpolate_for_num!(f64); interpolate_for_num!(f64);
pub trait SignalSamplePoints {
type Output<'a>: IntoIterator<Item = &'a Duration>
where
Self: 'a;
/// Get the time points where the signal is sampled.
fn time_points(&'_ self) -> Option<Self::Output<'_>>;
}
pub trait SignalSyncPoints<Rhs = Self> {
type Output<'a>: IntoIterator<Item = &'a Duration>
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<Self::Output<'a>>;
}
impl<T> SignalSamplePoints for Signal<T>
where
Signal<T>: BaseSignal,
T: Copy,
{
type Output<'a> = Vec<&'a Duration>
where
Self: 'a;
fn time_points(&'_ self) -> Option<Self::Output<'_>> {
if self.is_empty() {
None
} else {
self.time_points.iter().collect_vec().into()
}
}
}
impl<T> SignalSamplePoints for ConstantSignal<T>
where
T: Copy,
{
type Output<'a> = Empty<&'a Duration>
where
Self: 'a;
fn time_points(&'_ self) -> Option<Self::Output<'_>> {
if self.is_empty() {
None
} else {
core::iter::empty().into()
}
}
}
impl<T> SignalSyncPoints<Self> for Signal<T>
where
T: Copy,
Self: BaseSignal<Value = T>,
{
type Output<'a> = Vec<&'a Duration>
where
Self: 'a,
Self: 'a;
fn synchronization_points<'a>(&'a self, other: &'a Self) -> Option<Self::Output<'a>> {
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<T> SignalSyncPoints<ConstantSignal<T>> for Signal<T>
where
T: Copy,
Self: BaseSignal<Value = T>,
{
type Output<'a> = Vec<&'a Duration>
where
Self: 'a,
Self: 'a;
fn synchronization_points<'a>(&'a self, other: &'a ConstantSignal<T>) -> Option<Self::Output<'a>> {
if self.is_empty() || other.is_empty() {
return None;
}
self.time_points.iter().collect_vec().into()
}
}
// impl<T> SignalSyncPoints<ConstantSignal<T>> for ConstantSignal<T>
// where
// T: Copy,
// Self: BaseSignal<Value = T>,
// {
// type Output<'a> = Empty<&'a Duration>
// where
// Self: 'a,
// Self: 'a;
//
// fn synchronization_points<'a>(&'a self, _other: &'a ConstantSignal<T>) ->
// Option<Self::Output<'a>> { Some(core::iter::empty())
// }
// }
impl<T> SignalSyncPoints<Signal<T>> for ConstantSignal<T>
where
T: Copy,
Self: BaseSignal<Value = T>,
{
type Output<'a> = Vec<&'a Duration>
where
Self: 'a,
Self: 'a;
fn synchronization_points<'a>(&'a self, other: &'a Signal<T>) -> Option<Self::Output<'a>> {
other.synchronization_points(self)
}
}
macro_rules! impl_signal_cmp { macro_rules! impl_signal_cmp {
($cmp:ident) => { ($cmp:ident) => {
paste! { paste! {
fn [<signal_ $cmp>](&self, other: &Rhs) -> Option<Self::Output> { fn [<signal_ $cmp>](&self, other: &Rhs) -> Option<Signal<bool>> {
self.signal_cmp(other, |ord| ord.[<is_ $cmp>]()) self.signal_cmp(other, |ord| ord.[<is_ $cmp>]())
} }
} }
@ -307,15 +84,13 @@ macro_rules! impl_signal_cmp {
} }
/// A time-wise partial ordering defined for signals /// A time-wise partial ordering defined for signals
pub trait SignalPartialOrd<Rhs = Self>: BaseSignal { pub trait SignalPartialOrd<Rhs = Self> {
type Output: BaseSignal<Value = bool>;
/// Compare two signals within each of their domains (using [`PartialOrd`]) and /// Compare two signals within each of their domains (using [`PartialOrd`]) and
/// apply the given function `op` to the ordering to create a signal. /// apply the given function `op` to the ordering to create a signal.
/// ///
/// This function returns `None` if the comparison isn't possible, namely, when /// This function returns `None` if the comparison isn't possible, namely, when
/// either of the signals are empty. /// either of the signals are empty.
fn signal_cmp<F>(&self, other: &Rhs, op: F) -> Option<Self::Output> fn signal_cmp<F>(&self, other: &Rhs, op: F) -> Option<Signal<bool>>
where where
F: Fn(Ordering) -> bool; F: Fn(Ordering) -> bool;
@ -328,8 +103,8 @@ pub trait SignalPartialOrd<Rhs = Self>: BaseSignal {
} }
/// Time-wise min-max of signal types /// Time-wise min-max of signal types
pub trait SignalMinMax<Rhs = Self>: BaseSignal { pub trait SignalMinMax<Rhs = Self> {
type Output: BaseSignal; type Output;
/// Compute the time-wise min of two signals /// Compute the time-wise min of two signals
fn min(&self, rhs: &Rhs) -> Self::Output; fn min(&self, rhs: &Rhs) -> Self::Output;
@ -340,21 +115,16 @@ pub trait SignalMinMax<Rhs = Self>: BaseSignal {
/// Trait for converting between numeric signal types /// Trait for converting between numeric signal types
pub trait SignalNumCast { pub trait SignalNumCast {
type Value: Num + NumCast; fn to_i8(&self) -> Option<Signal<i8>>;
type Output<T>: BaseSignal<Value = T> fn to_i16(&self) -> Option<Signal<i16>>;
where fn to_i32(&self) -> Option<Signal<i32>>;
T: Num + NumCast + Copy; fn to_i64(&self) -> Option<Signal<i64>>;
fn to_u8(&self) -> Option<Signal<u8>>;
fn to_i8(&self) -> Option<Self::Output<i8>>; fn to_u16(&self) -> Option<Signal<u16>>;
fn to_i16(&self) -> Option<Self::Output<i16>>; fn to_u32(&self) -> Option<Signal<u32>>;
fn to_i32(&self) -> Option<Self::Output<i32>>; fn to_u64(&self) -> Option<Signal<u64>>;
fn to_i64(&self) -> Option<Self::Output<i64>>; fn to_f32(&self) -> Option<Signal<f32>>;
fn to_u8(&self) -> Option<Self::Output<u8>>; fn to_f64(&self) -> Option<Signal<f64>>;
fn to_u16(&self) -> Option<Self::Output<u16>>;
fn to_u32(&self) -> Option<Self::Output<u32>>;
fn to_u64(&self) -> Option<Self::Output<u64>>;
fn to_f32(&self) -> Option<Self::Output<f32>>;
fn to_f64(&self) -> Option<Self::Output<f64>>;
} }
/// Trait for computing the absolute value of the samples in a signal /// Trait for computing the absolute value of the samples in a signal

View file

@ -6,12 +6,12 @@
use core::ops::{Bound, RangeBounds}; use core::ops::{Bound, RangeBounds};
use core::time::Duration; use core::time::Duration;
use std::cmp::Ordering; use std::iter::zip;
use num_traits::NumCast; use num_traits::NumCast;
use super::traits::{LinearInterpolatable, SignalSyncPoints}; use super::traits::LinearInterpolatable;
use super::{BaseSignal, ConstantSignal, InterpolationMethod, Sample, Signal}; use super::{InterpolationMethod, Sample, Signal};
/// The neighborhood around a signal such that the time `at` is between the `first` and /// The neighborhood around a signal such that the time `at` is between the `first` and
/// `second` samples. /// `second` samples.
@ -61,108 +61,55 @@ where
Sample { time: t, value: y } Sample { time: t, value: y }
} }
/// Augment synchronization points with time points where signals intersect #[inline]
pub fn sync_with_intersection<'a, T, Sig1, Sig2>(sig1: &'a Sig1, sig2: &'a Sig2) -> Option<Vec<Duration>>
where
T: PartialOrd + Copy + NumCast + LinearInterpolatable,
Sig1: BaseSignal<Value = T> + SignalSyncPoints<Sig2>,
Sig2: BaseSignal<Value = T> + SignalSyncPoints<Sig1>,
{
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::<Duration>::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)
}
pub fn apply1<T, U, F>(signal: &Signal<T>, op: F) -> Signal<U> pub fn apply1<T, U, F>(signal: &Signal<T>, op: F) -> Signal<U>
where where
T: Copy, T: Copy,
F: Fn(T) -> U, F: Fn(T) -> U,
Signal<U>: std::iter::FromIterator<(Duration, U)>, Signal<U>: 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<T>, rhs: &'a Signal<T>, op: F) -> Signal<U> pub fn apply2<'a, T, U, F>(lhs: &'a Signal<T>, rhs: &'a Signal<T>, op: F) -> Signal<U>
where where
T: Copy + LinearInterpolatable, T: Copy + LinearInterpolatable,
U: Copy, U: Copy,
F: Fn(T, T) -> U, F: Fn(T, T) -> U,
{ {
use Signal::*;
// If either of the signals are empty, we return an empty signal. // If either of the signals are empty, we return an empty signal.
if lhs.is_empty() || rhs.is_empty() { if lhs.is_empty() || rhs.is_empty() {
// Intersection with empty signal should yield an empty signal // Intersection with empty signal should yield an empty signal
return Signal::<U>::new(); return Signal::<U>::new();
} }
// We determine the range of the signal (as the output signal can only be match (lhs, rhs) {
// defined in the domain where both signals are defined). // If either of the signals are empty, we return an empty signal.
let time_points = lhs.synchronization_points(rhs).unwrap(); (Empty, _) | (_, Empty) => Signal::new(),
// Now, at each of the merged time points, we sample each signal and operate on (Constant { value: v1 }, Constant { value: v2 }) => Signal::constant(op(*v1, *v2)),
// them (lhs, rhs) => {
time_points // We determine the range of the signal (as the output signal can only be
.into_iter() // defined in the domain where both signals are defined).
.map(|t| { let time_points = lhs.sync_points(rhs).unwrap();
let v1 = lhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap(); // Now, at each of the merged time points, we sample each signal and operate on
let v2 = rhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap(); // them
(*t, op(v1, v2)) time_points
}) .into_iter()
.collect() .map(|t| {
} let v1 = lhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap();
let v2 = rhs.interpolate_at(*t, InterpolationMethod::Linear).unwrap();
pub fn apply2_const<'a, T, U, F>(lhs: &'a Signal<T>, rhs: &'a ConstantSignal<T>, op: F) -> Signal<U> (*t, op(v1, v2))
where })
T: Copy + LinearInterpolatable, .collect()
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::<U>::new();
} }
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<T>(a: T, b: T) -> Option<T> fn partial_min<T>(a: T, b: T) -> Option<T>