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:
parent
a6a3805107
commit
4431b79bcd
10 changed files with 442 additions and 966 deletions
|
|
@ -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:?})"
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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,49 +196,67 @@ 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
|
||||||
fn at(&self, time: Duration) -> Option<&Self::Value> {
|
/// 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!(
|
assert_eq!(
|
||||||
self.time_points.len(),
|
time_points.len(),
|
||||||
self.values.len(),
|
values.len(),
|
||||||
"invariant: number of time points must equal number of samples"
|
"invariant: number of time points must equal number of samples"
|
||||||
);
|
);
|
||||||
// if there are no sample points, then there is no sample point (nor neighboring
|
// if there are no sample points, then there is no sample point (nor neighboring
|
||||||
// sample points) to return
|
// sample points) to return
|
||||||
if self.time_points.is_empty() {
|
if time_points.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will use binary search to find the appropriate index
|
// We will use binary search to find the appropriate index
|
||||||
match self.time_points.binary_search(&time) {
|
match time_points.binary_search(&time) {
|
||||||
Ok(idx) => self.values.get(idx),
|
Ok(idx) => values.get(idx),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
where
|
||||||
Self::Value: Copy + LinearInterpolatable,
|
T: Copy + LinearInterpolatable,
|
||||||
{
|
{
|
||||||
|
match self {
|
||||||
|
Signal::Empty => None,
|
||||||
|
Signal::Constant { value } => Some(*value),
|
||||||
|
Signal::Sampled { values, time_points } => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.time_points.len(),
|
time_points.len(),
|
||||||
self.values.len(),
|
values.len(),
|
||||||
"invariant: number of time points must equal number of samples"
|
"invariant: number of time points must equal number of samples"
|
||||||
);
|
);
|
||||||
// if there are no sample points, then there is no sample point (nor neighboring
|
// if there are no sample points, then there is no sample point (nor neighboring
|
||||||
// sample points) to return
|
// sample points) to return
|
||||||
if self.time_points.is_empty() {
|
if time_points.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will use binary search to find the appropriate index
|
// We will use binary search to find the appropriate index
|
||||||
let hint_idx = match self.time_points.binary_search(&time) {
|
let hint_idx = match time_points.binary_search(&time) {
|
||||||
Ok(idx) => return self.values.get(idx).copied(),
|
Ok(idx) => return values.get(idx).copied(),
|
||||||
Err(idx) => idx,
|
Err(idx) => idx,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -175,98 +268,130 @@ impl<T> BaseSignal for Signal<T> {
|
||||||
// (since we know that the signal is non-empty).
|
// (since we know that the signal is non-empty).
|
||||||
let preceding = None;
|
let preceding = None;
|
||||||
let following = Some(Sample {
|
let following = Some(Sample {
|
||||||
time: self.time_points[hint_idx],
|
time: time_points[hint_idx],
|
||||||
value: self.values[hint_idx],
|
value: values[hint_idx],
|
||||||
});
|
});
|
||||||
(preceding, following)
|
(preceding, following)
|
||||||
} else if hint_idx == self.time_points.len() {
|
} else if hint_idx == time_points.len() {
|
||||||
// Sample appears past the end of the signal
|
// Sample appears past the end of the signal
|
||||||
// So, let's return just the preceding sample, which is the last sample
|
// So, let's return just the preceding sample, which is the last sample
|
||||||
// (since we know the signal is non-empty)
|
// (since we know the signal is non-empty)
|
||||||
let preceding = Some(Sample {
|
let preceding = Some(Sample {
|
||||||
time: self.time_points[hint_idx - 1],
|
time: time_points[hint_idx - 1],
|
||||||
value: self.values[hint_idx - 1],
|
value: values[hint_idx - 1],
|
||||||
});
|
});
|
||||||
let following = None;
|
let following = None;
|
||||||
(preceding, following)
|
(preceding, following)
|
||||||
} else {
|
} else {
|
||||||
// The sample should exist within the signal.
|
// The sample should exist within the signal.
|
||||||
assert!(self.time_points.len() >= 2, "There should be at least 2 elements");
|
assert!(time_points.len() >= 2, "There should be at least 2 elements");
|
||||||
let preceding = Some(Sample {
|
let preceding = Some(Sample {
|
||||||
time: self.time_points[hint_idx - 1],
|
time: time_points[hint_idx - 1],
|
||||||
value: self.values[hint_idx - 1],
|
value: values[hint_idx - 1],
|
||||||
});
|
});
|
||||||
let following = Some(Sample {
|
let following = Some(Sample {
|
||||||
time: self.time_points[hint_idx],
|
time: time_points[hint_idx],
|
||||||
value: self.values[hint_idx],
|
value: values[hint_idx],
|
||||||
});
|
});
|
||||||
(preceding, following)
|
(preceding, following)
|
||||||
};
|
};
|
||||||
|
|
||||||
interp.at(time, &first, &second)
|
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> {
|
pub fn time_points(&self) -> Option<Vec<&Duration>> {
|
||||||
assert_eq!(self.time_points.len(), self.values.len());
|
match self {
|
||||||
|
Signal::Empty => None,
|
||||||
|
Signal::Constant { value: _ } => Vec::new().into(),
|
||||||
|
Signal::Sampled { values: _, time_points } => time_points.iter().collect_vec().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let last_time = self.time_points.last();
|
/// Return a list consisting of all the points where the two signals should be
|
||||||
match last_time {
|
/// sampled and synchronized for operations.
|
||||||
Some(last_t) if last_t >= &time => Err(Error::NonMonotonicSignal {
|
pub fn sync_points<'a>(&'a self, other: &'a Self) -> Option<Vec<&'a Duration>> {
|
||||||
end_time: *last_t,
|
use core::ops::Bound::*;
|
||||||
current_sample: time,
|
|
||||||
}),
|
if self.is_empty() || other.is_empty() {
|
||||||
_ => {
|
return None;
|
||||||
self.time_points.push(time);
|
}
|
||||||
self.values.push(value);
|
match (self, other) {
|
||||||
Ok(true)
|
(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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
/// Augment synchronization points with time points where signals intersect
|
||||||
pub struct ConstantSignal<T> {
|
pub fn sync_with_intersection(&self, other: &Signal<T>) -> Option<Vec<Duration>>
|
||||||
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>
|
|
||||||
where
|
where
|
||||||
Self::Value: Copy + LinearInterpolatable,
|
T: PartialOrd + Copy + LinearInterpolatable + NumCast,
|
||||||
{
|
{
|
||||||
Some(self.value)
|
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();
|
||||||
|
|
||||||
fn push(&mut self, _time: Duration, _value: Self::Value) -> ArgusResult<bool> {
|
// We will check for any intersections between the current sample and the
|
||||||
Ok(false)
|
// 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);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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| {
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,77 +61,43 @@ 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();
|
||||||
}
|
}
|
||||||
|
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
|
// We determine the range of the signal (as the output signal can only be
|
||||||
// defined in the domain where both signals are defined).
|
// defined in the domain where both signals are defined).
|
||||||
let time_points = lhs.synchronization_points(rhs).unwrap();
|
let time_points = lhs.sync_points(rhs).unwrap();
|
||||||
// Now, at each of the merged time points, we sample each signal and operate on
|
// Now, at each of the merged time points, we sample each signal and operate on
|
||||||
// them
|
// them
|
||||||
time_points
|
time_points
|
||||||
|
|
@ -142,27 +108,8 @@ where
|
||||||
(*t, op(v1, v2))
|
(*t, op(v1, v2))
|
||||||
})
|
})
|
||||||
.collect()
|
.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();
|
|
||||||
}
|
}
|
||||||
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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue