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")]
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:?})"

View file

@ -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};

View file

@ -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<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)]
pub enum InterpolationMethod {
Linear,
@ -76,31 +66,116 @@ pub struct Sample<T> {
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<T> {
pub(crate) values: Vec<T>,
pub(crate) time_points: Vec<Duration>,
/// 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<T> {
#[default]
Empty,
Constant {
value: T,
},
Sampled {
values: Vec<T>,
time_points: Vec<Duration>,
},
}
impl<T> Signal<T> {
/// 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<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.
pub fn iter(&self) -> impl Iterator<Item = (&Duration, &T)> {
self.into_iter()
@ -121,152 +196,202 @@ impl<T> Signal<T> {
}
Ok(signal)
}
}
impl<T> BaseSignal for Signal<T> {
type Value = T;
type Bounds = RangeInclusive<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).
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<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)
// 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<T> {
pub value: T,
}
impl<T> ConstantSignal<T> {
pub fn new(value: T) -> Self {
Self { value }
}
}
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>
/// Interpolate the value of the signal at the given time point
///
/// If there exists a sample at the given time point then `Some(value)` is returned
/// with the value of the signal at the point. Otherwise, a the
/// [`InterpolationMethod`] is used to compute the value. If the given interpolation
/// method cannot be used at the given time (for example, if we use
/// [`InterpolationMethod::Linear`] and the `time` point is outside the signal
/// domain), then a `None` is returned.
pub fn interpolate_at(&self, time: Duration, interp: InterpolationMethod) -> Option<T>
where
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> {
Ok(false)
pub fn time_points(&self) -> Option<Vec<&Duration>> {
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
pub fn constant_signal<T>() -> impl Strategy<Value = ConstantSignal<T>>
pub fn constant_signal<T>() -> impl Strategy<Value = Signal<T>>
where
T: Arbitrary,
T: Arbitrary + Copy,
{
any::<T>().prop_map(ConstantSignal::new)
}
/// 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),
]
any::<T>().prop_map(Signal::constant)
}
/// Generate an arbitrary signal
pub fn any_signal(size: impl Into<SizeRange> + Clone) -> impl Strategy<Value = AnySignal> {
prop_oneof![
constant_signal::<bool>().prop_map(AnySignal::from),
constant_signal::<i64>().prop_map(AnySignal::from),
constant_signal::<u64>().prop_map(AnySignal::from),
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),
]
pub fn signal<T>(size: impl Into<SizeRange>) -> impl Strategy<Value = Signal<T>>
where
T: Arbitrary + Copy,
{
prop_oneof![constant_signal::<T>(), sampled_signal::<T>(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),
}
}
}
};

View file

@ -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<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> {
type Output = Signal<bool>;
@ -32,50 +24,3 @@ impl core::ops::BitOr<Self> for &Signal<bool> {
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 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 [<to_ $type>](&self) -> Option<Signal<$type>> {
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 [<to_ $to>](&self) -> Option<Signal<$to>> {
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<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) => {
($from:ty => $to:ty) => {
paste::paste! {
#[inline]
fn [<to_ $type>](&self) -> Option<ConstantSignal<$type>> {
num_traits::cast::<_, $type>(self.value).map(ConstantSignal::new)
fn [<to_ $to>](&self) -> Option<Signal<$to>> {
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>
where
T: Num + NumCast + Copy,
{
type Value = T;
type Output<U> = ConstantSignal<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);
}
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 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<T> SignalPartialOrd<Self> for Signal<T>
where
T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable,
{
type Output = Signal<bool>;
fn signal_cmp<F>(&self, other: &Self, op: F) -> Option<Self::Output>
fn signal_cmp<F>(&self, other: &Self, op: F) -> Option<Signal<bool>>
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<bool> = sync_points
.into_iter()
.map(|t| {
@ -35,90 +32,14 @@ where
}
}
impl<T> SignalPartialOrd<ConstantSignal<T>> for Signal<T>
impl<T> SignalMinMax<Self> for Signal<T>
where
T: PartialOrd + Copy + std::fmt::Debug + NumCast + LinearInterpolatable,
{
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>,
T: PartialOrd + Copy + LinearInterpolatable + NumCast,
{
type Output = Signal<T>;
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| {

View file

@ -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, Duration>, core::slice::Iter<'a, T>>,
#[derive(Debug, Default)]
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> {
type Item = (&'a Duration, &'a T);
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;
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)),
}
}
}

View file

@ -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<T> core::ops::Neg for &Signal<T>
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>
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>;
@ -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>
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>;
@ -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>
where
T: core::ops::Sub<T, Output = T> + Num + NumCast + Copy + LinearInterpolatable + PartialOrd,
Signal<T>: BaseSignal<Value = T>,
T: core::ops::Sub<T, Output = T> + Copy + LinearInterpolatable + PartialOrd + NumCast,
{
type Output = Signal<T>;
@ -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<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();
let sync_points = self.sync_with_intersection(rhs).unwrap();
sync_points
.into_iter()
.map(|t| {
@ -225,7 +74,7 @@ where
impl<T> core::ops::Div for &Signal<T>
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>;
@ -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>
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>;
@ -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<u64> {
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::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<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;
}
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<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 {
($cmp:ident) => {
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>]())
}
}
@ -307,15 +84,13 @@ macro_rules! impl_signal_cmp {
}
/// A time-wise partial ordering defined for signals
pub trait SignalPartialOrd<Rhs = Self>: BaseSignal {
type Output: BaseSignal<Value = bool>;
pub trait SignalPartialOrd<Rhs = Self> {
/// 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<F>(&self, other: &Rhs, op: F) -> Option<Self::Output>
fn signal_cmp<F>(&self, other: &Rhs, op: F) -> Option<Signal<bool>>
where
F: Fn(Ordering) -> bool;
@ -328,8 +103,8 @@ pub trait SignalPartialOrd<Rhs = Self>: BaseSignal {
}
/// Time-wise min-max of signal types
pub trait SignalMinMax<Rhs = Self>: BaseSignal {
type Output: BaseSignal;
pub trait SignalMinMax<Rhs = Self> {
type Output;
/// Compute the time-wise min of two signals
fn min(&self, rhs: &Rhs) -> Self::Output;
@ -340,21 +115,16 @@ pub trait SignalMinMax<Rhs = Self>: BaseSignal {
/// Trait for converting between numeric signal types
pub trait SignalNumCast {
type Value: Num + NumCast;
type Output<T>: BaseSignal<Value = T>
where
T: Num + NumCast + Copy;
fn to_i8(&self) -> Option<Self::Output<i8>>;
fn to_i16(&self) -> Option<Self::Output<i16>>;
fn to_i32(&self) -> Option<Self::Output<i32>>;
fn to_i64(&self) -> Option<Self::Output<i64>>;
fn to_u8(&self) -> Option<Self::Output<u8>>;
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>>;
fn to_i8(&self) -> Option<Signal<i8>>;
fn to_i16(&self) -> Option<Signal<i16>>;
fn to_i32(&self) -> Option<Signal<i32>>;
fn to_i64(&self) -> Option<Signal<i64>>;
fn to_u8(&self) -> Option<Signal<u8>>;
fn to_u16(&self) -> Option<Signal<u16>>;
fn to_u32(&self) -> Option<Signal<u32>>;
fn to_u64(&self) -> Option<Signal<u64>>;
fn to_f32(&self) -> Option<Signal<f32>>;
fn to_f64(&self) -> Option<Signal<f64>>;
}
/// 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::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<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)
}
#[inline]
pub fn apply1<T, U, F>(signal: &Signal<T>, op: F) -> Signal<U>
where
T: Copy,
F: Fn(T) -> 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>
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::<U>::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<T>, rhs: &'a ConstantSignal<T>, op: F) -> Signal<U>
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::<U>::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<T>(a: T, b: T) -> Option<T>