refactor!(argus): combine co-dependent crates
- argus-core, argus-parser, argus-semantics are highly co-dependent, and hence should be in the same create.
This commit is contained in:
parent
dc71a51df3
commit
7ce056b471
43 changed files with 281 additions and 399 deletions
|
|
@ -9,6 +9,26 @@ rust-version.workspace = true
|
|||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
argus-core = { version = "0.1.1", path = "../argus-core" }
|
||||
argus-parser = { version = "0.1.1", path = "../argus-parser" }
|
||||
argus-semantics = { version = "0.1.1", path = "../argus-semantics" }
|
||||
argus-derive = { version = "0.1.0", path = "../argus-derive" }
|
||||
ariadne = { version = "0.3.0", optional = true }
|
||||
chumsky = { version = "1.0.0-alpha.4", features = ["default", "label"] }
|
||||
derive_more = "0.99.17"
|
||||
enum_dispatch = "0.3.12"
|
||||
itertools = "0.11"
|
||||
num-traits = "0.2.16"
|
||||
paste = "1.0.14"
|
||||
proptest = { version = "1.2", optional = true }
|
||||
thiserror = "1.0.47"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.2"
|
||||
argus = { path = ".", features = ["arbitrary"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
arbitrary = ["dep:proptest"]
|
||||
reporting = ["dep:ariadne"]
|
||||
|
||||
[[example]]
|
||||
name = "dump_expr"
|
||||
required-features = ["reporting"]
|
||||
|
|
|
|||
31
argus/examples/dump_expr.rs
Normal file
31
argus/examples/dump_expr.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use std::env;
|
||||
|
||||
use argus::parse_str;
|
||||
use ariadne::{sources, Color, Label, Report, ReportKind};
|
||||
|
||||
fn main() {
|
||||
let src = env::args().nth(1).expect("Expected expression");
|
||||
|
||||
match parse_str(&src) {
|
||||
Ok(expr) => println!("{:#?}", expr),
|
||||
Err(errs) => {
|
||||
errs.into_iter().for_each(|e| {
|
||||
Report::build(ReportKind::Error, src.clone(), e.span().start)
|
||||
.with_message(e.to_string())
|
||||
.with_label(
|
||||
Label::new((src.clone(), e.span().into_range()))
|
||||
.with_message(e.reason().to_string())
|
||||
.with_color(Color::Red),
|
||||
)
|
||||
.with_labels(e.contexts().map(|(label, span)| {
|
||||
Label::new((src.clone(), span.into_range()))
|
||||
.with_message(format!("while parsing this {}", label))
|
||||
.with_color(Color::Yellow)
|
||||
}))
|
||||
.finish()
|
||||
.eprint(sources([(src.clone(), src.clone())]))
|
||||
.unwrap()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
8
argus/proptest-regressions/core/expr/traits.txt
Normal file
8
argus/proptest-regressions/core/expr/traits.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 400240db9d2e9afecba257e48d5385b34a5dc746c60b34c1621a1f6e8c41893f # shrinks to num_expr = IntLit(IntLit(0))
|
||||
cc 8f1f212537f462eb0d9f46febda2f5d1c57b60596290a70e9acca0d4162e90f5 # shrinks to bool_expr = Not(Not { arg: BoolVar(BoolVar { name: "Cz_da6bc_347__" }) })
|
||||
8
argus/proptest-regressions/core/signals.txt
Normal file
8
argus/proptest-regressions/core/signals.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 9d75c17b5aea213ae64cde3da8939775416a4ae6ccdda51c2e54ac067612ce86 # shrinks to (mut samples, a, b) = ([(0ns, false), (3606793s, false), (225236632134049379s, false), (1452516483796720883s, true), (1454074610924622985s, true), (2261379572419890469s, true), (2346287628474660760s, true), (3370236408549918743s, false), (3885751221773112430s, true), (3978746064686161523s, false), (3993201691451842848s, true), (4400859595782393585s, false), (5595174780223578169s, true), (6103121693246216159s, true), (7083137466118103668s, true), (7762254907571717843s, false), (7925271027375436371s, false), (8116317203292488589s, false), (8304849594782901286s, false), (8624344724558373380s, false), (8815156223625440978s, false), (9387270278621444607s, true), (12063545309142768726s, true), (13147668321430668654s, false), (14456000981272753118s, true), (15332734450820787217s, true), (16183101507372578898s, false), (16240800821249987611s, false), (16620766479072627673s, false)], 0, 1)
|
||||
cc cad6474e3f3dd9ef9bb626b4ca7ea1348f1149dcd60bcdc4bd495b722339a545 # shrinks to sig1 = Signal { values: [4.3478034e-5, 1.6862512e-31, -4.1751028e-23, -3.5984213e29, -4.028606e-35, -2406548600000000.0, 0.0, -5.9627377e-33, 7.572252e29, 6019438.5, -1.3212671e35, 2.611877e-29, 5.8927715e-29, 1.1563975e-20, -0.0, -8.3151186e-27, -206.30678, 1.3011893e31, -2.8298384e23, -0.0, -1.059635e-39, -3.3279884e17, -0.0, 0.0, 56873.555, 2.5092264e-8, -5.993404e-24, 14222.387, -8.2869476e-35, -8.757546e31, 3.1672832e-30, -0.0, -3.5087894e-10, -0.00017120276, 2.2623386e35, -638976500.0, -105290824.0, 8.7764174e-17, 9.5735087e-32, -2.8903045e-29, -0.0, 122.85877, -9.545952e-33, 7.47762e33, -8.404993e24, 1.0348725e-38, 7.788807e37, 0.0, 1.4925183, -4.9916196e-38, 1.2545456e-11, 2.2975067e33, 4.0639423e37, -1.3042648e21, -8.95971e-39, 1.3585841e29, -6.9586837e19, 0.0, -9.653235e-39, -4.888808e26, 6.388707e24, -2.3371024e21, -181674120000.0, 0.10696149, -0.0008027013, 2.2801897e-16, -0.0, 0.0, 0.0, 9.825498e34, 0.0, -4.5300488e-20, -4.1823565e27, -0.0, 3.3528904e-5, 1.1041216e-38], time_points: [0ns, 1s, 2s, 3s, 4s, 5s, 6s, 7s, 8s, 9s, 10s, 113737847550694s, 102863897179245388s, 231940358149573730s, 985268944032287251s, 1179938249496372883s, 1523237204686644602s, 1585817810129733726s, 1721252945505149535s, 1965468420885560001s, 2769762976791412020s, 3252548403129826299s, 3292745570567771666s, 3443593595043352109s, 3533956933556232128s, 4010495723212981953s, 4182559278081799346s, 4243887477914787268s, 4572079607131917189s, 4714915074037568667s, 4758350708433182231s, 5431181482174886945s, 5552332237516185066s, 5814630114729607195s, 6204882717816206590s, 6463168719238326712s, 6482473141688098009s, 6693036037256699541s, 6745947616375351427s, 6942459590849043375s, 7084175984632141235s, 7214291908418839395s, 7772310116644908753s, 8155172457835178803s, 8403968785923892642s, 8515222007371616657s, 8859969392014259914s, 9084599327087020461s, 9651887062893308189s, 9725122416706352291s, 9947736190416544582s, 10080059396725911590s, 10299008642147421851s, 10685580210794221555s, 12015733219006163058s, 12065490407135319859s, 12226399543225878027s, 12241600173310080344s, 12357765645910189639s, 12362671363206900323s, 13144204948939894916s, 13170638657454114214s, 13235858652328420546s, 13323569105700310192s, 13429247049667427038s, 14168201301002478138s, 14475433465798767617s, 15235766071742689765s, 15264726464487596872s, 15455661560042436701s, 16085575839208887167s, 16307210977853972380s, 16515700279935854517s, 17063270879408061072s, 17347080806007301222s, 18188991381042633226s] }, sig2 = Signal { values: [0.0, -2.3270595e22, 0.0, -74220420000.0, -0.0015518349, -2.0560898e-16, 2.6638045e-28, -5.198415e-39, 2.3715333e-35, -0.0, -2.6957326e20, 1.0571348e17, -7.009875e20, -2028.2014, -3.373818e-18, -8.489281e-31, 0.0, 2.44124e-40, 0.0, 4.7543917e-35], time_points: [1188109279128335285s, 4336470717621492487s, 5604281200143793229s, 5753525323725675922s, 5997182151475702640s, 7114976848808910275s, 8162892512110790770s, 8325259808728253755s, 8795243898338924749s, 10477201318357830930s, 10889350696363419107s, 11434058078282998679s, 11716315439205885036s, 12633010086548416593s, 12857768907633317898s, 14202886989116169562s, 14475433465798767686s, 14601007326330154422s, 16278127428799016591s, 18026591724775596273s] }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 658412106dcfc0e507ce8c292021a7c519550eb07ac47b2fd459df14c8ffce31 # shrinks to (vec, width) = ([0, 0, 0], 2)
|
||||
643
argus/src/core/expr.rs
Normal file
643
argus/src/core/expr.rs
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
//! Expression tree for Argus specifications
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
mod bool_expr;
|
||||
pub mod iter;
|
||||
mod num_expr;
|
||||
mod traits;
|
||||
|
||||
pub use bool_expr::*;
|
||||
use enum_dispatch::enum_dispatch;
|
||||
pub use num_expr::*;
|
||||
pub use traits::*;
|
||||
|
||||
use self::iter::AstIter;
|
||||
use crate::{ArgusResult, Error};
|
||||
|
||||
/// A trait representing expressions
|
||||
#[enum_dispatch]
|
||||
pub trait AnyExpr {
|
||||
/// Check if the given expression is a numeric expression
|
||||
fn is_numeric(&self) -> bool;
|
||||
/// Check if the given expression is a boolean expression
|
||||
fn is_boolean(&self) -> bool;
|
||||
/// Get the arguments to the current expression.
|
||||
///
|
||||
/// If the expression doesn't contain arguments (i.e., it is a leaf expression) then
|
||||
/// the vector is empty.
|
||||
fn args(&self) -> Vec<ExprRef<'_>>;
|
||||
}
|
||||
|
||||
/// Marker trait for numeric expressions
|
||||
pub trait IsNumExpr: AnyExpr + Into<NumExpr> {}
|
||||
|
||||
/// Marker trait for Boolean expressions
|
||||
pub trait IsBoolExpr: AnyExpr + Into<BoolExpr> {}
|
||||
|
||||
/// All expressions that are numeric
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
#[enum_dispatch(AnyExpr)]
|
||||
pub enum NumExpr {
|
||||
/// A signed integer literal
|
||||
IntLit(IntLit),
|
||||
/// An unsigned integer literal
|
||||
UIntLit(UIntLit),
|
||||
/// A floating point literal
|
||||
FloatLit(FloatLit),
|
||||
/// A signed integer variable
|
||||
IntVar(IntVar),
|
||||
/// A unsigned integer variable
|
||||
UIntVar(UIntVar),
|
||||
/// A floating point number variable
|
||||
FloatVar(FloatVar),
|
||||
/// Numeric negation of a numeric expression
|
||||
Neg(Neg),
|
||||
/// Arithmetic addition of a list of numeric expressions
|
||||
Add(Add),
|
||||
/// Subtraction of two numbers
|
||||
Sub(Sub),
|
||||
/// Arithmetic multiplication of a list of numeric expressions
|
||||
Mul(Mul),
|
||||
/// Divide two expressions `dividend / divisor`
|
||||
Div(Div),
|
||||
/// The absolute value of an expression
|
||||
Abs(Abs),
|
||||
}
|
||||
|
||||
impl NumExpr {
|
||||
/// Create a borrowed iterator over the expression tree
|
||||
pub fn iter(&self) -> AstIter<'_> {
|
||||
AstIter::new(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// All expressions that are evaluated to be of type `bool`
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
#[enum_dispatch(AnyExpr)]
|
||||
pub enum BoolExpr {
|
||||
/// A `bool` literal
|
||||
BoolLit(BoolLit),
|
||||
/// A `bool` variable
|
||||
BoolVar(BoolVar),
|
||||
/// A comparison expression
|
||||
Cmp(Cmp),
|
||||
/// Logical negation of an expression
|
||||
Not(Not),
|
||||
/// Logical conjunction of a list of expressions
|
||||
And(And),
|
||||
/// Logical disjunction of a list of expressions
|
||||
Or(Or),
|
||||
|
||||
/// A temporal next expression
|
||||
///
|
||||
/// Checks if the next time point in a signal is `true` or not.
|
||||
Next(Next),
|
||||
|
||||
/// Temporal "oracle" expression
|
||||
///
|
||||
/// This is equivalent to `steps` number of nested [`Next`](BoolExpr::Next)
|
||||
/// expressions.
|
||||
Oracle(Oracle),
|
||||
|
||||
/// A temporal always expression
|
||||
///
|
||||
/// - If the `interval` is `(Unbounded, Unbounded)` or equivalent to `(0,
|
||||
/// Unbounded)`: checks if the signal is `true` for all points in a signal.
|
||||
/// - Otherwise: checks if the signal is `true` for all points within the
|
||||
/// `interval`.
|
||||
Always(Always),
|
||||
|
||||
/// A temporal eventually expression
|
||||
///
|
||||
/// - If the `interval` is `(Unbounded, Unbounded)` or equivalent to `(0,
|
||||
/// Unbounded)`: checks if the signal is `true` for some point in a signal.
|
||||
/// - Otherwise: checks if the signal is `true` for some point within the
|
||||
/// `interval`.
|
||||
Eventually(Eventually),
|
||||
|
||||
/// A temporal until expression
|
||||
///
|
||||
/// Checks if the `lhs` is always `true` for a signal until `rhs` becomes `true`.
|
||||
Until(Until),
|
||||
}
|
||||
|
||||
impl BoolExpr {
|
||||
/// Create a borrowed iterator over the expression tree
|
||||
pub fn iter(&self) -> AstIter<'_> {
|
||||
AstIter::new(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to an expression (either [`BoolExpr`] or [`NumExpr`]).
|
||||
#[derive(Clone, Copy, Debug, derive_more::From)]
|
||||
pub enum ExprRef<'a> {
|
||||
/// A reference to a [`BoolExpr`]
|
||||
Bool(&'a BoolExpr),
|
||||
/// A reference to a [`NumExpr`]
|
||||
Num(&'a NumExpr),
|
||||
}
|
||||
|
||||
/// An expression (either [`BoolExpr`] or [`NumExpr`])
|
||||
#[derive(Clone, Debug)]
|
||||
#[enum_dispatch(AnyExpr)]
|
||||
pub enum Expr {
|
||||
/// A reference to a [`BoolExpr`]
|
||||
Bool(BoolExpr),
|
||||
/// A reference to a [`NumExpr`]
|
||||
Num(NumExpr),
|
||||
}
|
||||
|
||||
/// Expression builder
|
||||
///
|
||||
/// The `ExprBuilder` is a factory structure that deals with the creation of
|
||||
/// expressions. The main goal of this is to ensure users do not create duplicate
|
||||
/// definitions for variables.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExprBuilder {
|
||||
declarations: HashSet<String>,
|
||||
}
|
||||
|
||||
impl ExprBuilder {
|
||||
/// Create a new `ExprBuilder` context.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
declarations: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare a constant boolean expression
|
||||
pub fn bool_const(&self, value: bool) -> BoolExpr {
|
||||
BoolLit(value).into()
|
||||
}
|
||||
|
||||
/// Declare a constant integer expression
|
||||
pub fn int_const(&self, value: i64) -> NumExpr {
|
||||
IntLit(value).into()
|
||||
}
|
||||
|
||||
/// Declare a constant unsigned integer expression
|
||||
pub fn uint_const(&self, value: u64) -> NumExpr {
|
||||
UIntLit(value).into()
|
||||
}
|
||||
|
||||
/// Declare a constant floating point expression
|
||||
pub fn float_const(&self, value: f64) -> NumExpr {
|
||||
FloatLit(value).into()
|
||||
}
|
||||
|
||||
/// Declare a boolean variable
|
||||
pub fn bool_var(&mut self, name: String) -> ArgusResult<BoolExpr> {
|
||||
if self.declarations.insert(name.clone()) {
|
||||
Ok((BoolVar { name }).into())
|
||||
} else {
|
||||
Err(Error::IdentifierRedeclaration)
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare a integer variable
|
||||
pub fn int_var(&mut self, name: String) -> ArgusResult<NumExpr> {
|
||||
if self.declarations.insert(name.clone()) {
|
||||
Ok((IntVar { name }).into())
|
||||
} else {
|
||||
Err(Error::IdentifierRedeclaration)
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare a unsigned integer variable
|
||||
pub fn uint_var(&mut self, name: String) -> ArgusResult<NumExpr> {
|
||||
if self.declarations.insert(name.clone()) {
|
||||
Ok((UIntVar { name }).into())
|
||||
} else {
|
||||
Err(Error::IdentifierRedeclaration)
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare a floating point variable
|
||||
pub fn float_var(&mut self, name: String) -> ArgusResult<NumExpr> {
|
||||
if self.declarations.insert(name.clone()) {
|
||||
Ok((FloatVar { name }).into())
|
||||
} else {
|
||||
Err(Error::IdentifierRedeclaration)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`NumExpr::Neg`] expression
|
||||
pub fn make_neg(&self, arg: Box<NumExpr>) -> NumExpr {
|
||||
(Neg { arg }).into()
|
||||
}
|
||||
|
||||
/// Create a [`NumExpr::Add`] expression
|
||||
pub fn make_add<I>(&self, args: I) -> ArgusResult<NumExpr>
|
||||
where
|
||||
I: IntoIterator<Item = NumExpr>,
|
||||
{
|
||||
let mut new_args = Vec::<NumExpr>::new();
|
||||
for arg in args.into_iter() {
|
||||
// Flatten the args if there is an Add
|
||||
if let NumExpr::Add(Add { args }) = arg {
|
||||
new_args.extend(args.into_iter());
|
||||
} else {
|
||||
new_args.push(arg);
|
||||
}
|
||||
}
|
||||
if new_args.len() < 2 {
|
||||
Err(Error::IncompleteArgs)
|
||||
} else {
|
||||
Ok((Add { args: new_args }).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`NumExpr::Mul`] expression
|
||||
pub fn make_mul<I>(&self, args: I) -> ArgusResult<NumExpr>
|
||||
where
|
||||
I: IntoIterator<Item = NumExpr>,
|
||||
{
|
||||
let mut new_args = Vec::<NumExpr>::new();
|
||||
for arg in args.into_iter() {
|
||||
// Flatten the args if there is a Mul
|
||||
if let NumExpr::Mul(Mul { args }) = arg {
|
||||
new_args.extend(args.into_iter());
|
||||
} else {
|
||||
new_args.push(arg);
|
||||
}
|
||||
}
|
||||
if new_args.len() < 2 {
|
||||
Err(Error::IncompleteArgs)
|
||||
} else {
|
||||
Ok((Mul { args: new_args }).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`NumExpr::Sub`] expression
|
||||
pub fn make_sub(&self, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> NumExpr {
|
||||
(Sub { lhs, rhs }).into()
|
||||
}
|
||||
|
||||
/// Create a [`NumExpr::Div`] expression
|
||||
pub fn make_div(&self, dividend: Box<NumExpr>, divisor: Box<NumExpr>) -> NumExpr {
|
||||
(Div { dividend, divisor }).into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Cmp`] expression
|
||||
pub fn make_cmp(&self, op: Ordering, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> BoolExpr {
|
||||
(Cmp { op, lhs, rhs }).into()
|
||||
}
|
||||
|
||||
/// Create a "less than" ([`BoolExpr::Cmp`]) expression
|
||||
pub fn make_lt(&self, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> BoolExpr {
|
||||
self.make_cmp(Ordering::Less { strict: true }, lhs, rhs)
|
||||
}
|
||||
|
||||
/// Create a "less than or equal" ([`BoolExpr::Cmp`]) expression
|
||||
pub fn make_le(&self, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> BoolExpr {
|
||||
self.make_cmp(Ordering::Less { strict: false }, lhs, rhs)
|
||||
}
|
||||
|
||||
/// Create a "greater than" ([`BoolExpr::Cmp`]) expression
|
||||
pub fn make_gt(&self, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> BoolExpr {
|
||||
self.make_cmp(Ordering::Greater { strict: true }, lhs, rhs)
|
||||
}
|
||||
|
||||
/// Create a "greater than or equal" ([`BoolExpr::Cmp`]) expression
|
||||
pub fn make_ge(&self, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> BoolExpr {
|
||||
self.make_cmp(Ordering::Greater { strict: false }, lhs, rhs)
|
||||
}
|
||||
|
||||
/// Create a "equals" ([`BoolExpr::Cmp`]) expression
|
||||
pub fn make_eq(&self, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> BoolExpr {
|
||||
self.make_cmp(Ordering::Eq, lhs, rhs)
|
||||
}
|
||||
|
||||
/// Create a "not equals" ([`BoolExpr::Cmp`]) expression
|
||||
pub fn make_neq(&self, lhs: Box<NumExpr>, rhs: Box<NumExpr>) -> BoolExpr {
|
||||
self.make_cmp(Ordering::NotEq, lhs, rhs)
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Not`] expression.
|
||||
pub fn make_not(&self, arg: Box<BoolExpr>) -> BoolExpr {
|
||||
(Not { arg }).into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Or`] expression.
|
||||
pub fn make_or<I>(&self, args: I) -> ArgusResult<BoolExpr>
|
||||
where
|
||||
I: IntoIterator<Item = BoolExpr>,
|
||||
{
|
||||
let mut new_args = Vec::<BoolExpr>::new();
|
||||
for arg in args.into_iter() {
|
||||
// Flatten the args if there is an Or
|
||||
if let BoolExpr::Or(Or { args }) = arg {
|
||||
new_args.extend(args.into_iter());
|
||||
} else {
|
||||
new_args.push(arg);
|
||||
}
|
||||
}
|
||||
if new_args.len() < 2 {
|
||||
Err(Error::IncompleteArgs)
|
||||
} else {
|
||||
Ok((Or { args: new_args }).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::And`] expression.
|
||||
pub fn make_and<I>(&self, args: I) -> ArgusResult<BoolExpr>
|
||||
where
|
||||
I: IntoIterator<Item = BoolExpr>,
|
||||
{
|
||||
let mut new_args = Vec::<BoolExpr>::new();
|
||||
for arg in args.into_iter() {
|
||||
// Flatten the args if there is an And
|
||||
if let BoolExpr::And(And { args }) = arg {
|
||||
new_args.extend(args.into_iter());
|
||||
} else {
|
||||
new_args.push(arg);
|
||||
}
|
||||
}
|
||||
if new_args.len() < 2 {
|
||||
Err(Error::IncompleteArgs)
|
||||
} else {
|
||||
Ok((And { args: new_args }).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an expression equivalent to `lhs -> rhs`.
|
||||
///
|
||||
/// This essentially breaks down the expression as `~lhs | rhs`.
|
||||
#[allow(clippy::boxed_local)]
|
||||
pub fn make_implies(&self, lhs: Box<BoolExpr>, rhs: Box<BoolExpr>) -> ArgusResult<BoolExpr> {
|
||||
let np = self.make_not(lhs);
|
||||
self.make_or([np, *rhs])
|
||||
}
|
||||
|
||||
/// Create an expression equivalent to `lhs <-> rhs`.
|
||||
///
|
||||
/// This essentially breaks down the expression as `(lhs & rhs) | (~lhs & ~rhs)`.
|
||||
pub fn make_equiv(&self, lhs: Box<BoolExpr>, rhs: Box<BoolExpr>) -> ArgusResult<BoolExpr> {
|
||||
let np = self.make_not(lhs.clone());
|
||||
let nq = self.make_not(rhs.clone());
|
||||
|
||||
let npnq = self.make_and([np, nq])?;
|
||||
let pq = self.make_and([*lhs, *rhs])?;
|
||||
|
||||
self.make_or([pq, npnq])
|
||||
}
|
||||
|
||||
/// Create an expression equivalent to `lhs ^ rhs`.
|
||||
///
|
||||
/// This essentially breaks down the expression as `~(lhs <-> rhs)`.
|
||||
pub fn make_xor(&self, lhs: Box<BoolExpr>, rhs: Box<BoolExpr>) -> ArgusResult<BoolExpr> {
|
||||
Ok(self.make_not(Box::new(self.make_equiv(lhs, rhs)?)))
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Next`] expression.
|
||||
pub fn make_next(&self, arg: Box<BoolExpr>) -> BoolExpr {
|
||||
(Next { arg }).into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Oracle`] expression.
|
||||
pub fn make_oracle(&self, steps: usize, arg: Box<BoolExpr>) -> BoolExpr {
|
||||
if steps == 1 {
|
||||
self.make_next(arg)
|
||||
} else {
|
||||
(Oracle { steps, arg }).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Always`] expression.
|
||||
pub fn make_always(&self, arg: Box<BoolExpr>) -> BoolExpr {
|
||||
(Always {
|
||||
arg,
|
||||
interval: (..).into(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Always`] expression with an interval.
|
||||
pub fn make_timed_always(&self, interval: Interval, arg: Box<BoolExpr>) -> BoolExpr {
|
||||
(Always { arg, interval }).into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Eventually`] expression.
|
||||
pub fn make_eventually(&self, arg: Box<BoolExpr>) -> BoolExpr {
|
||||
(Eventually {
|
||||
arg,
|
||||
interval: (..).into(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Eventually`] expression with an interval.
|
||||
pub fn make_timed_eventually(&self, interval: Interval, arg: Box<BoolExpr>) -> BoolExpr {
|
||||
(Eventually { arg, interval }).into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Until`] expression.
|
||||
pub fn make_until(&self, lhs: Box<BoolExpr>, rhs: Box<BoolExpr>) -> BoolExpr {
|
||||
(Until {
|
||||
lhs,
|
||||
rhs,
|
||||
interval: (..).into(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Create a [`BoolExpr::Until`] expression with an interval.
|
||||
pub fn make_timed_until(&self, interval: Interval, lhs: Box<BoolExpr>, rhs: Box<BoolExpr>) -> BoolExpr {
|
||||
BoolExpr::from(Until { lhs, rhs, interval })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "arbitrary"))]
|
||||
pub mod arbitrary {
|
||||
|
||||
//! Helper functions to generate arbitrary expressions using [`mod@proptest`].
|
||||
use core::ops::Bound;
|
||||
use core::time::Duration;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Generate arbitrary numeric expressions
|
||||
pub fn num_expr() -> impl Strategy<Value = Box<NumExpr>> {
|
||||
let leaf = prop_oneof![
|
||||
any::<i64>().prop_map(|val| Box::new(IntLit(val).into())),
|
||||
any::<u64>().prop_map(|val| Box::new(UIntLit(val).into())),
|
||||
any::<f64>().prop_map(|val| Box::new(FloatLit(val).into())),
|
||||
"[[:word:]]*".prop_map(|name| Box::new((IntVar { name }).into())),
|
||||
"[[:word:]]*".prop_map(|name| Box::new((UIntVar { name }).into())),
|
||||
"[[:word:]]*".prop_map(|name| Box::new((FloatVar { name }).into())),
|
||||
];
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
leaf.prop_recursive(
|
||||
8, // 8 levels deep
|
||||
128, // Shoot for maximum size of 256 nodes
|
||||
10, // We put up to 10 items per collection
|
||||
|inner| {
|
||||
prop_oneof![
|
||||
inner.clone().prop_map(|arg| Box::new((Neg { arg }).into())),
|
||||
prop::collection::vec(inner.clone(), 0..10).prop_map(|args| {
|
||||
Box::new(
|
||||
(Add {
|
||||
args: args.into_iter().map(|arg| *arg).collect(),
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
}),
|
||||
prop::collection::vec(inner.clone(), 0..10).prop_map(|args| {
|
||||
Box::new(
|
||||
(Mul {
|
||||
args: args.into_iter().map(|arg| *arg).collect(),
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
}),
|
||||
(inner.clone(), inner)
|
||||
.prop_map(|(dividend, divisor)| { Box::new((Div { dividend, divisor }).into()) })
|
||||
]
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate arbitrary comparison expressions
|
||||
pub fn cmp_expr() -> impl Strategy<Value = Box<BoolExpr>> {
|
||||
use Ordering::*;
|
||||
let op = prop_oneof![Just(Eq), Just(NotEq),];
|
||||
let lhs = num_expr();
|
||||
let rhs = num_expr();
|
||||
|
||||
(op, lhs, rhs).prop_map(|(op, lhs, rhs)| Box::new((Cmp { op, lhs, rhs }).into()))
|
||||
}
|
||||
|
||||
/// Generate arbitrary boolean expressions
|
||||
pub fn bool_expr() -> impl Strategy<Value = Box<BoolExpr>> {
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let leaf = prop_oneof![
|
||||
any::<bool>().prop_map(|val| Box::new(BoolLit(val).into())),
|
||||
"[[:word:]]*".prop_map(|name| Box::new((BoolVar { name }).into())),
|
||||
cmp_expr(),
|
||||
];
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
leaf.prop_recursive(
|
||||
8, // 8 levels deep
|
||||
128, // Shoot for maximum size of 256 nodes
|
||||
10, // We put up to 10 items per collection
|
||||
|inner| {
|
||||
let interval = (any::<(Bound<Duration>, Bound<Duration>)>()).prop_map_into::<Interval>();
|
||||
prop_oneof![
|
||||
inner.clone().prop_map(|arg| Box::new((Not { arg }).into())),
|
||||
prop::collection::vec(inner.clone(), 0..10).prop_map(|args| {
|
||||
Box::new(
|
||||
(And {
|
||||
args: args.into_iter().map(|arg| *arg).collect(),
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
}),
|
||||
prop::collection::vec(inner.clone(), 0..10).prop_map(|args| {
|
||||
Box::new(
|
||||
(Or {
|
||||
args: args.into_iter().map(|arg| *arg).collect(),
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
}),
|
||||
inner.clone().prop_map(|arg| Box::new((Next { arg }).into())),
|
||||
(inner.clone(), interval.clone())
|
||||
.prop_map(|(arg, interval)| Box::new((Always { arg, interval }).into())),
|
||||
(inner.clone(), interval.clone())
|
||||
.prop_map(|(arg, interval)| Box::new((Eventually { arg, interval }).into())),
|
||||
(inner.clone(), inner, interval)
|
||||
.prop_map(|(lhs, rhs, interval)| Box::new((Until { lhs, rhs, interval }).into())),
|
||||
]
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use paste::paste;
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn correctly_create_num_expr(num_expr in arbitrary::num_expr()) {
|
||||
_ = num_expr;
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn correctly_create_bool_expr(bool_expr in arbitrary::bool_expr()) {
|
||||
_ = bool_expr;
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn neg_num_expr(arg in arbitrary::num_expr()) {
|
||||
let expr = -*arg;
|
||||
assert!(matches!(expr, NumExpr::Neg(Neg { arg: _ })));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test_num_binop {
|
||||
($name:ident, $method:ident with /) => {
|
||||
paste! {
|
||||
proptest! {
|
||||
#[test]
|
||||
fn [<$method _num_expr>](lhs in arbitrary::num_expr(), rhs in arbitrary::num_expr()) {
|
||||
let expr = *lhs / *rhs;
|
||||
assert!(matches!(expr, NumExpr::$name($name {dividend: _, divisor: _ })));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($name:ident, $method:ident with $op:tt) => {
|
||||
paste! {
|
||||
proptest! {
|
||||
#[test]
|
||||
fn [<$method _num_expr>](lhs in arbitrary::num_expr(), rhs in arbitrary::num_expr()) {
|
||||
let expr = *lhs $op *rhs;
|
||||
assert!(matches!(expr, NumExpr::$name($name { args: _ })));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_num_binop!(Add, add with +);
|
||||
test_num_binop!(Mul, mul with *);
|
||||
test_num_binop!(Div, div with /);
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn not_bool_expr(arg in arbitrary::bool_expr()) {
|
||||
let expr = !*arg;
|
||||
assert!(matches!(expr, BoolExpr::Not(Not { arg: _ })));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test_bool_binop {
|
||||
($name:ident, $method:ident with $op:tt) => {
|
||||
paste! {
|
||||
proptest! {
|
||||
#[test]
|
||||
fn [<$method _bool_expr>](lhs in arbitrary::bool_expr(), rhs in arbitrary::bool_expr()) {
|
||||
let expr = *lhs $op *rhs;
|
||||
assert!(matches!(expr, BoolExpr::$name($name { args: _ })));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_bool_binop!(And, bitand with &);
|
||||
test_bool_binop!(Or, bitor with |);
|
||||
}
|
||||
294
argus/src/core/expr/bool_expr.rs
Normal file
294
argus/src/core/expr/bool_expr.rs
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
//! Boolean expression types
|
||||
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{AnyExpr, BoolExpr, NumExpr};
|
||||
|
||||
/// Types of comparison operations
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Ordering {
|
||||
/// Equality check for two expressions
|
||||
Eq,
|
||||
/// Non-equality check for two expressions
|
||||
NotEq,
|
||||
/// Less than check
|
||||
Less {
|
||||
/// Denotes `lhs < rhs` if `strict`, and `lhs <= rhs` otherwise.
|
||||
strict: bool,
|
||||
},
|
||||
/// Greater than check
|
||||
Greater {
|
||||
/// Denotes `lhs > rhs` if `strict`, and `lhs >= rhs` otherwise.
|
||||
strict: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Ordering {
|
||||
/// Check if `Ordering::Eq`
|
||||
pub fn equal() -> Self {
|
||||
Self::Eq
|
||||
}
|
||||
|
||||
/// Check if `Ordering::NotEq`
|
||||
pub fn not_equal() -> Self {
|
||||
Self::NotEq
|
||||
}
|
||||
|
||||
/// Check if `Ordering::Less { strict: true }`
|
||||
pub fn less_than() -> Self {
|
||||
Self::Less { strict: true }
|
||||
}
|
||||
|
||||
/// Check if `Ordering::Less { strict: false }`
|
||||
pub fn less_than_eq() -> Self {
|
||||
Self::Less { strict: false }
|
||||
}
|
||||
|
||||
/// Check if `Ordering::Greater { strict: true }`
|
||||
pub fn greater_than() -> Self {
|
||||
Self::Greater { strict: true }
|
||||
}
|
||||
|
||||
/// Check if `Ordering::Less { strict: false }`
|
||||
pub fn greater_than_eq() -> Self {
|
||||
Self::Greater { strict: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// A time interval for a temporal expression.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, derive_more::Into)]
|
||||
#[into(owned, ref, ref_mut)]
|
||||
pub struct Interval {
|
||||
/// Start of the interval
|
||||
pub start: Bound<Duration>,
|
||||
/// End of the interval
|
||||
pub end: Bound<Duration>,
|
||||
}
|
||||
|
||||
impl Interval {
|
||||
/// Create a new interval
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Argus doesn't permit `Interval`s with [`Bound::Excluded(_)`] values (as these
|
||||
/// can't be monitored reliably) and thus converts all such bounds to an
|
||||
/// [`Bound::Included(_)`]. Moreover, if the `start` bound is [`Bound::Unbounded`],
|
||||
/// it will get transformed to [`Bound::Included(Duration::ZERO)`].
|
||||
pub fn new(start: Bound<Duration>, end: Bound<Duration>) -> Self {
|
||||
use Bound::*;
|
||||
let start = match start {
|
||||
a @ Included(_) => a,
|
||||
Excluded(b) => Included(b),
|
||||
Unbounded => Included(Duration::ZERO),
|
||||
};
|
||||
|
||||
let end = match end {
|
||||
Excluded(b) => Included(b),
|
||||
bound => bound,
|
||||
};
|
||||
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
/// Check if the interval is empty
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
use Bound::*;
|
||||
match (&self.start, &self.end) {
|
||||
(Included(a), Included(b)) => a > b,
|
||||
(Included(a), Excluded(b)) | (Excluded(a), Included(b)) | (Excluded(a), Excluded(b)) => a >= b,
|
||||
(Unbounded, Excluded(b)) => b == &Duration::ZERO,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the interval is a singleton
|
||||
///
|
||||
/// This implies that only 1 timepoint is valid within this interval.
|
||||
#[inline]
|
||||
pub fn is_singleton(&self) -> bool {
|
||||
use Bound::*;
|
||||
match (&self.start, &self.end) {
|
||||
(Included(a), Included(b)) => a == b,
|
||||
(Unbounded, Included(b)) => b == &Duration::ZERO,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the interval covers `[0, ..)`.
|
||||
#[inline]
|
||||
pub fn is_untimed(&self) -> bool {
|
||||
use Bound::*;
|
||||
match (self.start, self.end) {
|
||||
(Unbounded, Unbounded) | (Included(Duration::ZERO), Unbounded) => true,
|
||||
(Included(_), Included(_)) | (Included(_), Unbounded) => false,
|
||||
(Excluded(_), _) | (_, Excluded(_)) | (Unbounded, _) => {
|
||||
unreachable!("looks like someone didn't use Interval::new")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Interval
|
||||
where
|
||||
T: RangeBounds<Duration>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value.start_bound().cloned(), value.end_bound().cloned())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(anand): Can I implement this within argus_derive?
|
||||
macro_rules! impl_bool_expr {
|
||||
($ty:ty$(, $($arg:ident),* )? ) => {
|
||||
impl AnyExpr for $ty {
|
||||
fn is_numeric(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_boolean(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<super::ExprRef<'_>> {
|
||||
vec![$($( self.$arg.as_ref().into(), )* )*]
|
||||
}
|
||||
}
|
||||
};
|
||||
($ty:ty, [$args:ident]) => {
|
||||
impl AnyExpr for $ty {
|
||||
fn is_numeric(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_boolean(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<super::ExprRef<'_>> {
|
||||
self.$args.iter().map(|arg| arg.into()).collect()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A `bool` literal
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct BoolLit(pub bool);
|
||||
|
||||
impl_bool_expr!(BoolLit);
|
||||
|
||||
/// A `bool` variable
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct BoolVar {
|
||||
/// Variable name
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl_bool_expr!(BoolVar);
|
||||
|
||||
/// A comparison expression
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Cmp {
|
||||
/// The type of comparison
|
||||
pub op: Ordering,
|
||||
/// The LHS for the comparison
|
||||
pub lhs: Box<NumExpr>,
|
||||
/// The RHS for the comparison
|
||||
pub rhs: Box<NumExpr>,
|
||||
}
|
||||
|
||||
impl_bool_expr!(Cmp, lhs, rhs);
|
||||
|
||||
/// Logical negation of an expression
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Not {
|
||||
/// Expression to be negated
|
||||
pub arg: Box<BoolExpr>,
|
||||
}
|
||||
|
||||
impl_bool_expr!(Not, arg);
|
||||
|
||||
/// Logical conjunction of a list of expressions
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct And {
|
||||
/// Expressions to be "and"-ed
|
||||
pub args: Vec<BoolExpr>,
|
||||
}
|
||||
|
||||
impl_bool_expr!(And, [args]);
|
||||
|
||||
/// Logical disjunction of a list of expressions
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Or {
|
||||
/// Expressions to be "or"-ed
|
||||
pub args: Vec<BoolExpr>,
|
||||
}
|
||||
|
||||
impl_bool_expr!(Or, [args]);
|
||||
|
||||
/// A temporal next expression
|
||||
///
|
||||
/// Checks if the next time point in a signal is `true` or not.
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Next {
|
||||
/// Argument for `Next`
|
||||
pub arg: Box<BoolExpr>,
|
||||
}
|
||||
impl_bool_expr!(Next, arg);
|
||||
|
||||
/// Temporal "oracle" expression
|
||||
///
|
||||
/// This is equivalent to `steps` number of nested [`Next`](BoolExpr::Next)
|
||||
/// expressions.
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Oracle {
|
||||
/// Number of steps to look ahead
|
||||
pub steps: usize,
|
||||
/// Argument for `Oracle`
|
||||
pub arg: Box<BoolExpr>,
|
||||
}
|
||||
impl_bool_expr!(Oracle, arg);
|
||||
|
||||
/// A temporal always expression
|
||||
///
|
||||
/// - If the `interval` is `(Unbounded, Unbounded)` or equivalent to `(0, Unbounded)`:
|
||||
/// checks if the signal is `true` for all points in a signal.
|
||||
/// - Otherwise: checks if the signal is `true` for all points within the `interval`.
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Always {
|
||||
/// Argument for `Always`
|
||||
pub arg: Box<BoolExpr>,
|
||||
/// Interval for the expression
|
||||
pub interval: Interval,
|
||||
}
|
||||
impl_bool_expr!(Always, arg);
|
||||
|
||||
/// A temporal eventually expression
|
||||
///
|
||||
/// - If the `interval` is `(Unbounded, Unbounded)` or equivalent to `(0, Unbounded)`:
|
||||
/// checks if the signal is `true` for some point in a signal.
|
||||
/// - Otherwise: checks if the signal is `true` for some point within the `interval`.
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Eventually {
|
||||
/// Argument for `Eventually`
|
||||
pub arg: Box<BoolExpr>,
|
||||
/// Interval for the expression
|
||||
pub interval: Interval,
|
||||
}
|
||||
impl_bool_expr!(Eventually, arg);
|
||||
|
||||
/// A temporal until expression
|
||||
///
|
||||
/// Checks if the `lhs` is always `true` for a signal until `rhs` becomes `true`.
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::BoolExpr)]
|
||||
pub struct Until {
|
||||
/// LHS to `lhs Until rhs`
|
||||
pub lhs: Box<BoolExpr>,
|
||||
/// RHS to `lhs Until rhs`
|
||||
pub rhs: Box<BoolExpr>,
|
||||
/// Interval for the expression
|
||||
pub interval: Interval,
|
||||
}
|
||||
impl_bool_expr!(Until, lhs, rhs);
|
||||
85
argus/src/core/expr/iter.rs
Normal file
85
argus/src/core/expr/iter.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
//! Iterators for expression trees
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::{AnyExpr, ExprRef};
|
||||
|
||||
/// Iterator that starts from some root [`AnyExpr`] and travels down to it's leaf
|
||||
/// expressions.
|
||||
///
|
||||
/// This essentially implements breadth-first search over the expression tree rooted at
|
||||
/// the given [`AnyExpr`].
|
||||
pub struct AstIter<'a> {
|
||||
queue: VecDeque<ExprRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> AstIter<'a> {
|
||||
/// Create an iterator that traverses an [`AnyExpr`] from root to leaf.
|
||||
pub fn new(root: ExprRef<'a>) -> Self {
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(root);
|
||||
Self { queue }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AstIter<'a> {
|
||||
type Item = ExprRef<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let expr_ref = self.queue.pop_front()?;
|
||||
|
||||
let expr: &dyn AnyExpr = match expr_ref {
|
||||
ExprRef::Bool(expr) => expr,
|
||||
ExprRef::Num(expr) => expr,
|
||||
};
|
||||
|
||||
// We need to get all the arguments of the current expression (not including
|
||||
// any intervals), and push them into the queue.
|
||||
for arg in expr.args().into_iter() {
|
||||
self.queue.push_back(arg);
|
||||
}
|
||||
// 4. Give the user their expr
|
||||
Some(expr_ref)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::expr::{ExprBuilder, ExprRef};
|
||||
|
||||
#[test]
|
||||
fn simple_iter() {
|
||||
let mut ctx = ExprBuilder::new();
|
||||
|
||||
let x = Box::new(ctx.float_var("x".to_owned()).unwrap());
|
||||
let y = Box::new(ctx.float_var("y".to_owned()).unwrap());
|
||||
let lit = Box::new(ctx.float_const(2.0));
|
||||
|
||||
let pred1 = Box::new(ctx.make_le(x.clone(), lit.clone()));
|
||||
let pred2 = Box::new(ctx.make_gt(y.clone(), lit.clone()));
|
||||
let spec = Box::new(ctx.make_or([*pred1.clone(), *pred2.clone()]).unwrap());
|
||||
|
||||
drop(ctx);
|
||||
|
||||
let expr_tree = spec.iter();
|
||||
let expected: Vec<ExprRef<'_>> = vec![
|
||||
spec.as_ref().into(),
|
||||
pred1.as_ref().into(),
|
||||
pred2.as_ref().into(),
|
||||
x.as_ref().into(),
|
||||
lit.as_ref().into(),
|
||||
y.as_ref().into(),
|
||||
lit.as_ref().into(),
|
||||
];
|
||||
|
||||
for (lhs, rhs) in expr_tree.zip_eq(expected.into_iter()) {
|
||||
match (lhs, rhs) {
|
||||
(ExprRef::Bool(lhs), ExprRef::Bool(rhs)) => assert_eq!(lhs, rhs),
|
||||
(ExprRef::Num(lhs), ExprRef::Num(rhs)) => assert_eq!(lhs, rhs),
|
||||
e => panic!("got mismatched pair: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
argus/src/core/expr/num_expr.rs
Normal file
128
argus/src/core/expr/num_expr.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Numeric expression types
|
||||
|
||||
use super::{AnyExpr, NumExpr};
|
||||
|
||||
// TODO(anand): Can I implement this within argus_derive?
|
||||
macro_rules! impl_num_expr {
|
||||
($ty:ty$(, $($arg:ident),* )? ) => {
|
||||
impl AnyExpr for $ty {
|
||||
fn is_numeric(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_boolean(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<super::ExprRef<'_>> {
|
||||
vec![$($( self.$arg.as_ref().into(), )* )*]
|
||||
}
|
||||
}
|
||||
};
|
||||
($ty:ty, [$args:ident]) => {
|
||||
impl AnyExpr for $ty {
|
||||
fn is_numeric(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_boolean(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<super::ExprRef<'_>> {
|
||||
self.$args.iter().map(|arg| arg.into()).collect()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A signed integer literal
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct IntLit(pub i64);
|
||||
impl_num_expr!(IntLit);
|
||||
|
||||
/// An unsigned integer literal
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct UIntLit(pub u64);
|
||||
impl_num_expr!(UIntLit);
|
||||
|
||||
/// A floating point literal
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct FloatLit(pub f64);
|
||||
impl_num_expr!(FloatLit);
|
||||
|
||||
/// A signed integer variable
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct IntVar {
|
||||
/// Name of the variable
|
||||
pub name: String,
|
||||
}
|
||||
impl_num_expr!(IntVar);
|
||||
|
||||
/// A unsigned integer variable
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct UIntVar {
|
||||
/// Name of the variable
|
||||
pub name: String,
|
||||
}
|
||||
impl_num_expr!(UIntVar);
|
||||
|
||||
/// A floating point number variable
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct FloatVar {
|
||||
/// Name of the variable
|
||||
pub name: String,
|
||||
}
|
||||
impl_num_expr!(FloatVar);
|
||||
|
||||
/// Numeric negation of a numeric expression
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct Neg {
|
||||
/// Numeric expression being negated
|
||||
pub arg: Box<NumExpr>,
|
||||
}
|
||||
impl_num_expr!(Neg, arg);
|
||||
|
||||
/// Arithmetic addition of a list of numeric expressions
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct Add {
|
||||
/// List of expressions being added
|
||||
pub args: Vec<NumExpr>,
|
||||
}
|
||||
impl_num_expr!(Add, [args]);
|
||||
|
||||
/// Subtraction of two numbers
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct Sub {
|
||||
/// LHS to the expression `lhs - rhs`
|
||||
pub lhs: Box<NumExpr>,
|
||||
/// RHS to the expression `lhs - rhs`
|
||||
pub rhs: Box<NumExpr>,
|
||||
}
|
||||
impl_num_expr!(Sub, lhs, rhs);
|
||||
|
||||
/// Arithmetic multiplication of a list of numeric expressions
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct Mul {
|
||||
/// List of expressions being multiplied
|
||||
pub args: Vec<NumExpr>,
|
||||
}
|
||||
impl_num_expr!(Mul, [args]);
|
||||
|
||||
/// Divide two expressions `dividend / divisor`
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct Div {
|
||||
/// The dividend
|
||||
pub dividend: Box<NumExpr>,
|
||||
/// The divisor
|
||||
pub divisor: Box<NumExpr>,
|
||||
}
|
||||
impl_num_expr!(Div, dividend, divisor);
|
||||
|
||||
/// The absolute value of an expression
|
||||
#[derive(Clone, Debug, PartialEq, argus_derive::NumExpr)]
|
||||
pub struct Abs {
|
||||
/// Argument to `abs`
|
||||
pub arg: Box<NumExpr>,
|
||||
}
|
||||
impl_num_expr!(Abs, arg);
|
||||
1
argus/src/core/expr/traits.rs
Normal file
1
argus/src/core/expr/traits.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
15
argus/src/core/mod.rs
Normal file
15
argus/src/core/mod.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//! # `argus-core`
|
||||
//!
|
||||
//! This crate provides some of the core functionality or interfaces for the other Argus
|
||||
//! components. Mainly, the crate provides:
|
||||
//!
|
||||
//! 1. Expression tree nodes for defining temporal logic specifications (see [`expr`]).
|
||||
//! 2. Different signal types for generating traces of data (see [`signals`]).
|
||||
//! 3. A list of possible errors any component in Argus can generate (see
|
||||
//! [`enum@Error`]).
|
||||
|
||||
pub mod expr;
|
||||
pub mod signals;
|
||||
|
||||
pub use expr::*;
|
||||
pub use signals::Signal;
|
||||
653
argus/src/core/signals.rs
Normal file
653
argus/src/core/signals.rs
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
//! Argus signals
|
||||
mod bool_ops;
|
||||
mod cast;
|
||||
mod cmp_ops;
|
||||
pub mod interpolation;
|
||||
pub mod iter;
|
||||
mod num_ops;
|
||||
mod shift_ops;
|
||||
pub mod traits;
|
||||
mod utils;
|
||||
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
use std::time::Duration;
|
||||
|
||||
use itertools::Itertools;
|
||||
use num_traits::Num;
|
||||
|
||||
pub use self::bool_ops::*;
|
||||
pub use self::cast::*;
|
||||
pub use self::cmp_ops::*;
|
||||
pub use self::num_ops::*;
|
||||
pub use self::shift_ops::*;
|
||||
pub use self::traits::*;
|
||||
use self::utils::intersect_bounds;
|
||||
use crate::{ArgusResult, Error};
|
||||
|
||||
/// A single sample of a signal.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Sample<T> {
|
||||
/// The time point when this sample was taken.
|
||||
pub time: Duration,
|
||||
/// The value of the signal at th given sample.
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
/// An empty signal.
|
||||
///
|
||||
/// This is only used in special (usually error) scenarios.
|
||||
#[default]
|
||||
Empty,
|
||||
/// A signal that has a constant value for the entire time domain.
|
||||
Constant {
|
||||
/// The value of the signal for all time.
|
||||
value: T,
|
||||
},
|
||||
/// A finite set of signal values sampled at strictly monotonically increasing time
|
||||
/// points.
|
||||
Sampled {
|
||||
/// Values of the samples of the signal.
|
||||
values: Vec<T>,
|
||||
/// List of time points where the signal is sampled.
|
||||
time_points: Vec<Duration>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T> Signal<T> {
|
||||
/// Create a new empty signal
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self::Empty
|
||||
}
|
||||
|
||||
/// Create a new constant signal
|
||||
#[inline]
|
||||
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::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()
|
||||
}
|
||||
|
||||
/// Try to create a signal from the input iterator
|
||||
///
|
||||
/// Returns an `Err` if the input samples are not in strictly monotonically
|
||||
/// increasing order.
|
||||
pub fn try_from_iter<I>(iter: I) -> ArgusResult<Self>
|
||||
where
|
||||
I: IntoIterator<Item = (Duration, T)>,
|
||||
{
|
||||
let iter = iter.into_iter();
|
||||
let mut signal = Signal::new_with_capacity(iter.size_hint().0);
|
||||
for (time, value) in iter.into_iter() {
|
||||
signal.push(time, value)?;
|
||||
}
|
||||
Ok(signal)
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
// We will use binary search to find the appropriate index
|
||||
match time_points.binary_search(&time) {
|
||||
Ok(idx) => values.get(idx),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the vector of points where the signal is sampled.
|
||||
///
|
||||
/// - If the signal is empty ([`Signal::Empty`]), the output is `None`.
|
||||
/// - If the signal is constant ([`Signal::Constant`]), the output is equivalent to
|
||||
/// `Some(vec![])` as there is no well defined list of sample points for a
|
||||
/// constant signal.
|
||||
/// - Finally, if the signal is sampled ([`Signal::Sampled`]), the output is the
|
||||
/// list of time points corresponding to the samples in the signal.
|
||||
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<Interp>(&self, other: &Signal<T>) -> Option<Vec<Duration>>
|
||||
where
|
||||
T: PartialOrd + Clone,
|
||||
Interp: InterpolationMethod<T>,
|
||||
{
|
||||
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
|
||||
.interpolate_at::<Interp>(*t)
|
||||
.unwrap_or_else(|| panic!("value must be present at given time {:?}.", t));
|
||||
let rhs = other
|
||||
.interpolate_at::<Interp>(*t)
|
||||
.unwrap_or_else(|| panic!("value must be present at given time {:?}.", t));
|
||||
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
|
||||
.interpolate_at::<Interp>(tm1)
|
||||
.map(|value| Sample { time: tm1, value }),
|
||||
second: self
|
||||
.interpolate_at::<Interp>(*t)
|
||||
.map(|value| Sample { time: *t, value }),
|
||||
};
|
||||
let b = utils::Neighborhood {
|
||||
first: other
|
||||
.interpolate_at::<Interp>(tm1)
|
||||
.map(|value| Sample { time: tm1, value }),
|
||||
second: other
|
||||
.interpolate_at::<Interp>(*t)
|
||||
.map(|value| Sample { time: *t, value }),
|
||||
};
|
||||
let intersect = Interp::find_intersection(&a, &b)
|
||||
.unwrap_or_else(|| panic!("unable to find intersection for crossing signals"));
|
||||
return_points.push(intersect.time);
|
||||
}
|
||||
}
|
||||
return_points.push(*t);
|
||||
last_sample = Some((*t, ord));
|
||||
}
|
||||
return_points.dedup();
|
||||
return_points.shrink_to_fit();
|
||||
Some(return_points)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Signal<T> {
|
||||
/// 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
|
||||
/// [`interpolation::Linear`] and the `time` point is outside the signal
|
||||
/// domain), then a `None` is returned.
|
||||
pub fn interpolate_at<Interp>(&self, time: Duration) -> Option<T>
|
||||
where
|
||||
Interp: InterpolationMethod<T>,
|
||||
{
|
||||
match self {
|
||||
Signal::Empty => None,
|
||||
Signal::Constant { value } => Some(value.clone()),
|
||||
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).cloned(),
|
||||
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.
|
||||
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).
|
||||
Some(values[hint_idx].clone())
|
||||
} 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)
|
||||
Some(values[hint_idx - 1].clone())
|
||||
} else {
|
||||
// The sample should exist within the signal.
|
||||
assert!(time_points.len() >= 2, "There should be at least 2 elements");
|
||||
let first = Sample {
|
||||
time: time_points[hint_idx - 1],
|
||||
value: values[hint_idx - 1].clone(),
|
||||
};
|
||||
let second = Sample {
|
||||
time: time_points[hint_idx],
|
||||
value: values[hint_idx].clone(),
|
||||
};
|
||||
Interp::at(&first, &second, time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Num> Signal<T> {
|
||||
/// Create a constant `0` signal
|
||||
pub fn zero() -> Self {
|
||||
Signal::constant(T::zero())
|
||||
}
|
||||
|
||||
/// Create a constant `1` signal
|
||||
pub fn one() -> Self {
|
||||
Signal::constant(T::one())
|
||||
}
|
||||
}
|
||||
|
||||
impl Signal<bool> {
|
||||
/// Create a constant `true` signal.
|
||||
pub fn const_true() -> Self {
|
||||
Signal::constant(true)
|
||||
}
|
||||
|
||||
/// Create a constant `false` signal.
|
||||
pub fn const_false() -> Self {
|
||||
Signal::constant(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "arbitrary"))]
|
||||
pub mod arbitrary {
|
||||
//! In this module, we use [`mod@proptest`] to define arbitrary generators for
|
||||
//! different signals.
|
||||
use proptest::prelude::*;
|
||||
use proptest::sample::SizeRange;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Generate an arbitrary list of samples and two indices within the list
|
||||
pub fn samples_and_indices<T>(
|
||||
size: impl Into<SizeRange>,
|
||||
) -> impl Strategy<Value = (Vec<(Duration, T)>, usize, usize)>
|
||||
where
|
||||
T: Arbitrary + Copy,
|
||||
{
|
||||
samples(size).prop_flat_map(|vec| {
|
||||
let len = vec.len();
|
||||
if len == 0 {
|
||||
(Just(vec), 0..1, 0..1)
|
||||
} else {
|
||||
(Just(vec), 0..len, 0..len)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate arbitrary samples for a signal where the time stamps are strictly
|
||||
/// monotonically increasing
|
||||
pub fn samples<T>(size: impl Into<SizeRange>) -> impl Strategy<Value = Vec<(Duration, T)>>
|
||||
where
|
||||
T: Arbitrary + Copy,
|
||||
{
|
||||
prop::collection::vec(any::<T>(), size).prop_flat_map(|values| {
|
||||
let len = values.len();
|
||||
prop::collection::vec(any::<u64>(), len).prop_map(move |mut ts| {
|
||||
ts.sort_unstable();
|
||||
ts.dedup();
|
||||
ts.into_iter()
|
||||
.map(Duration::from_secs)
|
||||
.zip(values.clone())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate arbitrary finite-length signals with samples of the given type
|
||||
pub fn sampled_signal<T>(size: impl Into<SizeRange>) -> impl Strategy<Value = Signal<T>>
|
||||
where
|
||||
T: Arbitrary + Copy,
|
||||
{
|
||||
samples(size).prop_map(Signal::<T>::from_iter)
|
||||
}
|
||||
|
||||
/// Generate an arbitrary constant signal
|
||||
pub fn constant_signal<T>() -> impl Strategy<Value = Signal<T>>
|
||||
where
|
||||
T: Arbitrary + Copy,
|
||||
{
|
||||
any::<T>().prop_map(Signal::constant)
|
||||
}
|
||||
|
||||
/// Generate an arbitrary signal
|
||||
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),]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::ops::Bound;
|
||||
|
||||
use paste::paste;
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
macro_rules! correctly_create_signals_impl {
|
||||
($ty:ty) => {
|
||||
proptest! {
|
||||
|((samples, idx, _) in arbitrary::samples_and_indices::<$ty>(0..100))| {
|
||||
// Creating a signal should be fine
|
||||
let signal: Signal<_> = samples.clone().into_iter().collect();
|
||||
|
||||
if samples.len() > 0 {
|
||||
// We wil get the start and end times.
|
||||
let start_time = samples.first().unwrap().0;
|
||||
let end_time = samples.last().unwrap().0;
|
||||
// Get the value of the sample at a given index
|
||||
let (at, val) = samples[idx];
|
||||
|
||||
assert_eq!(signal.start_time(), 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);
|
||||
} else {
|
||||
assert!(signal.is_empty());
|
||||
assert_eq!(signal.at(Duration::from_secs(1)), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
|((mut samples, a, b) in arbitrary::samples_and_indices::<$ty>(5..100))| {
|
||||
prop_assume!(a != b);
|
||||
// Swap two indices in the samples
|
||||
samples.swap(a, b);
|
||||
// Creating a signal should fail
|
||||
let signal = Signal::try_from_iter(samples.clone());
|
||||
assert!(signal.is_err(), "swapped {:?} and {:?}", samples[a], samples[b]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_signals_from_samples() {
|
||||
correctly_create_signals_impl!(bool);
|
||||
correctly_create_signals_impl!(i8);
|
||||
correctly_create_signals_impl!(i16);
|
||||
correctly_create_signals_impl!(i32);
|
||||
correctly_create_signals_impl!(i64);
|
||||
correctly_create_signals_impl!(u8);
|
||||
correctly_create_signals_impl!(u16);
|
||||
correctly_create_signals_impl!(u32);
|
||||
correctly_create_signals_impl!(u64);
|
||||
correctly_create_signals_impl!(f32);
|
||||
correctly_create_signals_impl!(f64);
|
||||
}
|
||||
|
||||
macro_rules! signals_fromiter_panic {
|
||||
($ty:ty) => {
|
||||
paste! {
|
||||
proptest! {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn [<fail_create_ $ty _signal>] ((mut samples, a, b) in arbitrary::samples_and_indices::<$ty>(5..100))
|
||||
{
|
||||
prop_assume!(a != b);
|
||||
// Swap two indices in the samples
|
||||
samples.swap(a, b);
|
||||
// Creating a signal should fail
|
||||
let _: Signal<_> = samples.into_iter().collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
signals_fromiter_panic!(bool);
|
||||
signals_fromiter_panic!(i8);
|
||||
signals_fromiter_panic!(i16);
|
||||
signals_fromiter_panic!(i32);
|
||||
signals_fromiter_panic!(i64);
|
||||
signals_fromiter_panic!(u8);
|
||||
signals_fromiter_panic!(u16);
|
||||
signals_fromiter_panic!(u32);
|
||||
signals_fromiter_panic!(u64);
|
||||
signals_fromiter_panic!(f32);
|
||||
signals_fromiter_panic!(f64);
|
||||
|
||||
macro_rules! signal_ops_impl {
|
||||
($ty:ty, $op:tt sig) => {
|
||||
proptest! {
|
||||
|(sig in arbitrary::sampled_signal::<$ty>(1..100))| {
|
||||
use interpolation::Linear;
|
||||
let new_sig = $op (&sig);
|
||||
for (t, v) in new_sig.iter() {
|
||||
let prev = sig.interpolate_at::<Linear>(*t).unwrap();
|
||||
assert_eq!($op prev, *v);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($ty:ty, lhs $op:tt rhs) => {
|
||||
proptest! {
|
||||
|(sig1 in arbitrary::sampled_signal::<$ty>(1..100), sig2 in arbitrary::sampled_signal::<$ty>(1..100))| {
|
||||
use interpolation::Linear;
|
||||
let new_sig = &sig1 $op &sig2;
|
||||
for (t, v) in new_sig.iter() {
|
||||
let v1 = sig1.interpolate_at::<Linear>(*t).unwrap();
|
||||
let v2 = sig2.interpolate_at::<Linear>(*t).unwrap();
|
||||
assert_eq!(v1 $op v2, *v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
|(sig1 in arbitrary::sampled_signal::<$ty>(1..100), sig2 in arbitrary::constant_signal::<$ty>())| {
|
||||
use interpolation::Linear;
|
||||
let new_sig = &sig1 $op &sig2;
|
||||
for (t, v) in new_sig.iter() {
|
||||
let v1 = sig1.interpolate_at::<Linear>(*t).unwrap();
|
||||
let v2 = sig2.interpolate_at::<Linear>(*t).unwrap();
|
||||
assert_eq!(v1 $op v2, *v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
|(sig1 in arbitrary::constant_signal::<$ty>(), sig2 in arbitrary::constant_signal::<$ty>())| {
|
||||
let new_sig = &sig1 $op &sig2;
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_ops() {
|
||||
signal_ops_impl!(bool, !sig);
|
||||
signal_ops_impl!(bool, lhs | rhs);
|
||||
signal_ops_impl!(bool, lhs & rhs);
|
||||
|
||||
// signal_ops_impl!(u64, lhs + rhs);
|
||||
// signal_ops_impl!(u64, lhs * rhs);
|
||||
// signal_ops_impl!(u64, lhs / rhs);
|
||||
|
||||
// signal_ops_impl!(i64, -sig);
|
||||
// signal_ops_impl!(i64, lhs + rhs);
|
||||
// signal_ops_impl!(i64, lhs * rhs);
|
||||
// signal_ops_impl!(i64, lhs / rhs);
|
||||
|
||||
// signal_ops_impl!(f32, -sig);
|
||||
// signal_ops_impl!(f32, lhs + rhs);
|
||||
// signal_ops_impl!(f32, lhs * rhs);
|
||||
// signal_ops_impl!(f32, lhs / rhs);
|
||||
|
||||
// signal_ops_impl!(f64, -sig);
|
||||
// signal_ops_impl!(f64, lhs + rhs);
|
||||
// signal_ops_impl!(f64, lhs * rhs);
|
||||
// signal_ops_impl!(f64, lhs / rhs);
|
||||
}
|
||||
}
|
||||
78
argus/src/core/signals/bool_ops.rs
Normal file
78
argus/src/core/signals/bool_ops.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use super::interpolation::Linear;
|
||||
use super::InterpolationMethod;
|
||||
use crate::signals::Signal;
|
||||
|
||||
impl core::ops::Not for Signal<bool> {
|
||||
type Output = Signal<bool>;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
self.logical_not()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Not for &Signal<bool> {
|
||||
type Output = Signal<bool>;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
self.logical_not()
|
||||
}
|
||||
}
|
||||
|
||||
impl Signal<bool> {
|
||||
/// Apply logical not for each sample across the signal.
|
||||
pub fn logical_not(&self) -> Self {
|
||||
self.unary_op(|&v| !v)
|
||||
}
|
||||
|
||||
/// Apply logical conjunction for each sample across the two signals.
|
||||
///
|
||||
/// Here, the conjunction is taken at all signal points where either of the signals
|
||||
/// are sampled, and where they intersect (using interpolation).
|
||||
///
|
||||
/// See [`Signal::sync_with_intersection`].
|
||||
pub fn and<I: InterpolationMethod<bool>>(&self, other: &Self) -> Self {
|
||||
self.binary_op::<_, _, I>(other, |lhs, rhs| *lhs && *rhs)
|
||||
}
|
||||
|
||||
/// Apply logical disjunction for each sample across the two signals.
|
||||
///
|
||||
/// Here, the disjunction is taken at all signal points where either of the signals
|
||||
/// are sampled, and where they intersect (using interpolation).
|
||||
///
|
||||
/// See [`Signal::sync_with_intersection`].
|
||||
pub fn or<I: InterpolationMethod<bool>>(&self, other: &Self) -> Self {
|
||||
self.binary_op::<_, _, I>(other, |lhs, rhs| *lhs || *rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::BitAnd<&Signal<bool>> for Signal<bool> {
|
||||
type Output = Signal<bool>;
|
||||
|
||||
fn bitand(self, other: &Signal<bool>) -> Self::Output {
|
||||
self.and::<Linear>(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::BitAnd<&Signal<bool>> for &Signal<bool> {
|
||||
type Output = Signal<bool>;
|
||||
|
||||
fn bitand(self, other: &Signal<bool>) -> Self::Output {
|
||||
self.and::<Linear>(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::BitOr<&Signal<bool>> for Signal<bool> {
|
||||
type Output = Signal<bool>;
|
||||
|
||||
fn bitor(self, other: &Signal<bool>) -> Self::Output {
|
||||
self.or::<Linear>(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::BitOr<&Signal<bool>> for &Signal<bool> {
|
||||
type Output = Signal<bool>;
|
||||
|
||||
fn bitor(self, other: &Signal<bool>) -> Self::Output {
|
||||
self.or::<Linear>(other)
|
||||
}
|
||||
}
|
||||
28
argus/src/core/signals/cast.rs
Normal file
28
argus/src/core/signals/cast.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use core::iter::zip;
|
||||
|
||||
use crate::signals::Signal;
|
||||
use crate::{ArgusError, ArgusResult};
|
||||
|
||||
impl<T> Signal<T>
|
||||
where
|
||||
T: num_traits::NumCast + Copy,
|
||||
{
|
||||
/// Cast a numeric signal to another numeric signal
|
||||
pub fn num_cast<U>(&self) -> ArgusResult<Signal<U>>
|
||||
where
|
||||
U: num_traits::NumCast,
|
||||
{
|
||||
let ret: Option<_> = match self {
|
||||
Signal::Empty => Some(Signal::Empty),
|
||||
Signal::Constant { value } => num_traits::cast(*value).map(Signal::constant),
|
||||
Signal::Sampled { values, time_points } => zip(time_points, values)
|
||||
.map(|(&t, &v)| {
|
||||
let val: U = num_traits::cast(v)?;
|
||||
Some((t, val))
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
ret.ok_or(ArgusError::invalid_cast::<T, U>())
|
||||
}
|
||||
}
|
||||
77
argus/src/core/signals/cmp_ops.rs
Normal file
77
argus/src/core/signals/cmp_ops.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use super::traits::SignalPartialOrd;
|
||||
use super::{InterpolationMethod, Signal};
|
||||
|
||||
impl<T> SignalPartialOrd<T> for Signal<T>
|
||||
where
|
||||
T: PartialOrd + Clone,
|
||||
{
|
||||
fn signal_cmp<F, I>(&self, other: &Self, op: F) -> Option<Signal<bool>>
|
||||
where
|
||||
F: Fn(Ordering) -> bool,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
// 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 = self.sync_with_intersection::<I>(other)?;
|
||||
let sig: Option<Signal<bool>> = sync_points
|
||||
.into_iter()
|
||||
.map(|t| {
|
||||
let lhs = self.interpolate_at::<I>(t).unwrap();
|
||||
let rhs = other.interpolate_at::<I>(t).unwrap();
|
||||
let cmp = lhs.partial_cmp(&rhs);
|
||||
cmp.map(|v| (t, op(v)))
|
||||
})
|
||||
.collect();
|
||||
sig
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Signal<T>
|
||||
where
|
||||
T: PartialOrd + Clone,
|
||||
{
|
||||
/// Compute the time-wise min of two signals
|
||||
pub fn min<I>(&self, other: &Self) -> Self
|
||||
where
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
let time_points = self.sync_with_intersection::<I>(other).unwrap();
|
||||
time_points
|
||||
.into_iter()
|
||||
.map(|t| {
|
||||
let lhs = self.interpolate_at::<I>(t).unwrap();
|
||||
let rhs = other.interpolate_at::<I>(t).unwrap();
|
||||
if lhs < rhs {
|
||||
(t, lhs)
|
||||
} else {
|
||||
(t, rhs)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compute the time-wise max of two signals
|
||||
pub fn max<I>(&self, other: &Self) -> Self
|
||||
where
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
let time_points = self.sync_with_intersection::<I>(other).unwrap();
|
||||
time_points
|
||||
.into_iter()
|
||||
.map(|t| {
|
||||
let lhs = self.interpolate_at::<I>(t).unwrap();
|
||||
let rhs = other.interpolate_at::<I>(t).unwrap();
|
||||
if lhs > rhs {
|
||||
(t, lhs)
|
||||
} else {
|
||||
(t, rhs)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
194
argus/src/core/signals/interpolation.rs
Normal file
194
argus/src/core/signals/interpolation.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
//! Interpolation methods
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::utils::Neighborhood;
|
||||
use super::{InterpolationMethod, Sample};
|
||||
|
||||
/// Constant interpolation.
|
||||
///
|
||||
/// Here, the previous signal value is propagated to the requested time point.
|
||||
pub struct Constant;
|
||||
|
||||
impl<T: Clone> InterpolationMethod<T> for Constant {
|
||||
fn at(a: &Sample<T>, b: &Sample<T>, time: Duration) -> Option<T> {
|
||||
if time == b.time {
|
||||
Some(b.value.clone())
|
||||
} else if a.time <= time && time < b.time {
|
||||
Some(a.value.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn find_intersection(_a: &Neighborhood<T>, _b: &Neighborhood<T>) -> Option<Sample<T>> {
|
||||
// The signals must be either constant or colinear. Thus, return None.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Nearest interpolation.
|
||||
///
|
||||
/// Here, the signal value from the nearest sample (time-wise) is propagated to the
|
||||
/// requested time point.
|
||||
pub struct Nearest;
|
||||
|
||||
impl<T: Clone> InterpolationMethod<T> for Nearest {
|
||||
fn at(a: &super::Sample<T>, b: &super::Sample<T>, time: std::time::Duration) -> Option<T> {
|
||||
if time < a.time || time > b.time {
|
||||
// `time` is outside the segments.
|
||||
None
|
||||
} else if (b.time - time) > (time - a.time) {
|
||||
// a is closer to the required time than b
|
||||
Some(a.value.clone())
|
||||
} else {
|
||||
// b is closer
|
||||
Some(b.value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn find_intersection(_a: &Neighborhood<T>, _b: &Neighborhood<T>) -> Option<Sample<T>> {
|
||||
// For the same reason as Constant interpolation, the signals must be either parallel or
|
||||
// colinear.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Linear interpolation.
|
||||
///
|
||||
/// Here, linear interpolation is performed to estimate the sample at the time point
|
||||
/// between two samples.
|
||||
pub struct Linear;
|
||||
|
||||
impl InterpolationMethod<bool> for Linear {
|
||||
fn at(a: &Sample<bool>, b: &Sample<bool>, time: Duration) -> Option<bool> {
|
||||
if a.time < time && time < b.time {
|
||||
// We can't linear interpolate a boolean, so we return the previous.
|
||||
Some(a.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn find_intersection(a: &Neighborhood<bool>, b: &Neighborhood<bool>) -> Option<Sample<bool>> {
|
||||
let Sample { time: ta1, value: ya1 } = a.first.unwrap();
|
||||
let Sample { time: ta2, value: ya2 } = a.second.unwrap();
|
||||
let Sample { time: tb1, value: yb1 } = b.first.unwrap();
|
||||
let Sample { time: tb2, value: yb2 } = b.second.unwrap();
|
||||
|
||||
let left_cmp = ya1.cmp(&yb1);
|
||||
let right_cmp = ya2.cmp(&yb2);
|
||||
|
||||
if left_cmp.is_eq() {
|
||||
// They already intersect, so we return the inner time-point
|
||||
if ta1 < tb1 {
|
||||
Some(Sample { time: tb1, value: yb1 })
|
||||
} else {
|
||||
Some(Sample { time: ta1, value: ya1 })
|
||||
}
|
||||
} else if right_cmp.is_eq() {
|
||||
// They intersect at the end, so we return the outer time-point, as that is
|
||||
// when they become equal.
|
||||
if ta2 < tb2 {
|
||||
Some(Sample { time: tb2, value: yb2 })
|
||||
} else {
|
||||
Some(Sample { time: ta2, value: ya2 })
|
||||
}
|
||||
} else if let (Ordering::Less, Ordering::Greater) | (Ordering::Greater, Ordering::Less) = (left_cmp, right_cmp)
|
||||
{
|
||||
// The switched, so the one that switched earlier will intersect with the
|
||||
// other.
|
||||
// So, we find the one that has a lower time point, i.e., the inner one.
|
||||
if ta2 < tb2 {
|
||||
Some(Sample { time: ta2, value: ya2 })
|
||||
} else {
|
||||
Some(Sample { time: tb2, value: yb2 })
|
||||
}
|
||||
} else {
|
||||
// The lines must be parallel.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! interpolate_for_num {
|
||||
($ty:ty) => {
|
||||
impl InterpolationMethod<$ty> for Linear {
|
||||
fn at(first: &Sample<$ty>, second: &Sample<$ty>, time: Duration) -> Option<$ty> {
|
||||
use num_traits::cast;
|
||||
// We will need to cast the samples to f64 values (along with the time
|
||||
// window) to be able to interpolate correctly.
|
||||
// TODO(anand): Verify this works.
|
||||
let t1 = first.time.as_secs_f64();
|
||||
let t2 = second.time.as_secs_f64();
|
||||
let at = time.as_secs_f64();
|
||||
assert!((t1..=t2).contains(&at));
|
||||
|
||||
// We need to do stable linear interpolation
|
||||
// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0811r3.html
|
||||
let a: f64 = cast(first.value).unwrap_or_else(|| panic!("unable to cast {:?} to f64", first.value));
|
||||
let b: f64 = cast(second.value).unwrap_or_else(|| panic!("unable to cast {:?} to f64", second.value));
|
||||
|
||||
// Set t to a value in [0, 1]
|
||||
let t = (at - t1) / (t2 - t1);
|
||||
assert!((0.0..=1.0).contains(&t));
|
||||
|
||||
let val = if (a <= 0.0 && b >= 0.0) || (a >= 0.0 && b <= 0.0) {
|
||||
t * b + (1.0 - t) * a
|
||||
} else if t == 1.0 {
|
||||
b
|
||||
} else {
|
||||
a + t * (b - a)
|
||||
};
|
||||
|
||||
cast(val)
|
||||
}
|
||||
|
||||
fn find_intersection(a: &Neighborhood<$ty>, b: &Neighborhood<$ty>) -> Option<Sample<$ty>> {
|
||||
// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
|
||||
use num_traits::cast;
|
||||
|
||||
let Sample { time: t1, value: y1 } = (a.first).unwrap();
|
||||
let Sample { time: t2, value: y2 } = (a.second).unwrap();
|
||||
let Sample { time: t3, value: y3 } = (b.first).unwrap();
|
||||
let Sample { time: t4, value: y4 } = (b.second).unwrap();
|
||||
|
||||
let t1 = t1.as_secs_f64();
|
||||
let t2 = t2.as_secs_f64();
|
||||
let t3 = t3.as_secs_f64();
|
||||
let t4 = t4.as_secs_f64();
|
||||
|
||||
let y1: f64 = cast(y1).unwrap_or_else(|| panic!("unable to cast {:?} to f64", y1));
|
||||
let y2: f64 = cast(y2).unwrap_or_else(|| panic!("unable to cast {:?} to f64", y2));
|
||||
let y3: f64 = cast(y3).unwrap_or_else(|| panic!("unable to cast {:?} to f64", y3));
|
||||
let y4: f64 = cast(y4).unwrap_or_else(|| panic!("unable to cast {:?} to f64", y4));
|
||||
|
||||
let denom = ((t1 - t2) * (y3 - y4)) - ((y1 - y2) * (t3 - t4));
|
||||
if denom.abs() <= 1e-10 {
|
||||
// The lines may be parallel or coincident.
|
||||
// We just return None
|
||||
return None;
|
||||
}
|
||||
|
||||
let t_top = (((t1 * y2) - (y1 * t2)) * (t3 - t4)) - ((t1 - t2) * (t3 * y4 - y3 * t4));
|
||||
let y_top = (((t1 * y2) - (y1 * t2)) * (y3 - y4)) - ((y1 - y2) * (t3 * y4 - y3 * t4));
|
||||
|
||||
let t = Duration::from_secs_f64(t_top / denom);
|
||||
let y: $ty = num_traits::cast(y_top / denom).unwrap();
|
||||
Some(Sample { time: t, value: y })
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interpolate_for_num!(i8);
|
||||
interpolate_for_num!(i16);
|
||||
interpolate_for_num!(i32);
|
||||
interpolate_for_num!(i64);
|
||||
interpolate_for_num!(u8);
|
||||
interpolate_for_num!(u16);
|
||||
interpolate_for_num!(u32);
|
||||
interpolate_for_num!(u64);
|
||||
interpolate_for_num!(f32);
|
||||
interpolate_for_num!(f64);
|
||||
57
argus/src/core/signals/iter.rs
Normal file
57
argus/src/core/signals/iter.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//! Signal iterator.
|
||||
|
||||
use std::iter::{zip, Zip};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::Signal;
|
||||
|
||||
/// An iterator over a [`Signal`].
|
||||
///
|
||||
/// This takes into account if the signal is iterable or not, i.e., it produces samples
|
||||
/// only for [`Signal::Sampled`] and empty iterators for [`Signal::Empty`] (as it
|
||||
/// contains no values) and [`Signal::Constant`] (as there is no well defined start and
|
||||
/// end to the signal).
|
||||
#[derive(Debug, Default)]
|
||||
pub enum Iter<'a, T> {
|
||||
#[doc(hidden)]
|
||||
#[default]
|
||||
Empty,
|
||||
#[doc(hidden)]
|
||||
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> {
|
||||
match self {
|
||||
Iter::Empty => None,
|
||||
Iter::Iter(iter) => iter.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a Signal<T> {
|
||||
type IntoIter = Iter<'a, T>;
|
||||
type Item = <Self::IntoIter as Iterator>::Item;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
match self {
|
||||
Signal::Empty => Iter::default(),
|
||||
Signal::Constant { value: _ } => Iter::default(),
|
||||
Signal::Sampled { values, time_points } => Iter::Iter(zip(time_points, values)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromIterator<(Duration, T)> for Signal<T> {
|
||||
/// Takes a sequence of sample points and creates a signal.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the input data does not contain strictly monotonically increasing time
|
||||
/// stamps. If this isn't desired, sort and deduplicate the input data.
|
||||
fn from_iter<I: IntoIterator<Item = (Duration, T)>>(iter: I) -> Self {
|
||||
Self::try_from_iter(iter).unwrap()
|
||||
}
|
||||
}
|
||||
113
argus/src/core/signals/num_ops.rs
Normal file
113
argus/src/core/signals/num_ops.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use super::{InterpolationMethod, SignalAbs};
|
||||
use crate::signals::Signal;
|
||||
|
||||
impl<T> Signal<T> {
|
||||
/// Perform sample-wise arithmetic negation over the signal.
|
||||
pub fn negate<U>(&self) -> Signal<U>
|
||||
where
|
||||
for<'a> &'a T: core::ops::Neg<Output = U>,
|
||||
{
|
||||
self.unary_op(|v| -v)
|
||||
}
|
||||
|
||||
/// Perform sample-wise addition of the two signals.
|
||||
///
|
||||
/// Here, a new point is computed for all signal points where either of the signals
|
||||
/// are sampled and where they intersect (using interpolation).
|
||||
pub fn add<U, I>(&self, other: &Signal<T>) -> Signal<U>
|
||||
where
|
||||
for<'t> &'t T: core::ops::Add<Output = U>,
|
||||
T: Clone,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
self.binary_op::<_, _, I>(other, |u, v| u + v)
|
||||
}
|
||||
|
||||
/// Perform sample-wise multiplication of the two signals.
|
||||
///
|
||||
/// Here, a new point is computed for all signal points where either of the signals
|
||||
/// are sampled and where they intersect (using interpolation).
|
||||
pub fn mul<U, I>(&self, other: &Signal<T>) -> Signal<U>
|
||||
where
|
||||
for<'t> &'t T: core::ops::Mul<Output = U>,
|
||||
T: Clone,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
self.binary_op::<_, _, I>(other, |u, v| u * v)
|
||||
}
|
||||
|
||||
/// Perform sample-wise subtraction of the two signals.
|
||||
///
|
||||
/// Here, a new point is computed for all signal points where either of the signals
|
||||
/// are sampled and where they intersect (using interpolation).
|
||||
pub fn sub<U, I>(&self, other: &Signal<T>) -> Signal<U>
|
||||
where
|
||||
for<'t> &'t T: core::ops::Sub<Output = U>,
|
||||
T: Clone + PartialOrd,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
self.binary_op_with_intersection::<_, _, I>(other, |u, v| u - v)
|
||||
}
|
||||
|
||||
/// Perform sample-wise division of the two signals.
|
||||
///
|
||||
/// Here, a new point is computed for all signal points where either of the signals
|
||||
/// are sampled and where they intersect (using interpolation).
|
||||
pub fn div<U, I>(&self, other: &Signal<T>) -> Signal<U>
|
||||
where
|
||||
for<'t> &'t T: core::ops::Div<Output = U>,
|
||||
T: Clone,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
self.binary_op::<_, _, I>(other, |u, v| u / v)
|
||||
}
|
||||
|
||||
/// Perform sample-wise exponentiation of the two signals.
|
||||
///
|
||||
/// Here, a new point is computed for all signal points where either of the signals
|
||||
/// are sampled and where they intersect (using interpolation).
|
||||
pub fn pow<U, I>(&self, exponent: &Signal<T>) -> Signal<U>
|
||||
where
|
||||
for<'a, 'b> &'a T: num_traits::Pow<&'b T, Output = U>,
|
||||
T: Clone,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
use num_traits::Pow;
|
||||
self.binary_op::<_, _, I>(exponent, |u, v| u.pow(v))
|
||||
}
|
||||
|
||||
/// Perform sample-wise absolute difference of the two signals.
|
||||
///
|
||||
/// Here, a new point is computed for all signal points where either of the signals
|
||||
/// are sampled and where they intersect (using interpolation).
|
||||
pub fn abs_diff<U, I>(&self, other: &Signal<T>) -> Signal<U>
|
||||
where
|
||||
for<'t> &'t T: core::ops::Sub<Output = U>,
|
||||
T: Clone + PartialOrd,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
self.binary_op_with_intersection::<_, _, I>(other, |u, v| if u < v { v - u } else { u - v })
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! signal_abs_impl {
|
||||
($( $ty:ty ), *) => {
|
||||
$(
|
||||
impl SignalAbs for Signal<$ty> {
|
||||
/// Return the absolute value for each sample in the signal
|
||||
fn abs(self) -> Signal<$ty> {
|
||||
self.unary_op(|v| v.abs())
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
signal_abs_impl!(i64, f32, f64);
|
||||
|
||||
impl SignalAbs for Signal<u64> {
|
||||
/// Return the absolute value for each sample in the signal
|
||||
fn abs(self) -> Signal<u64> {
|
||||
self.unary_op(|&v| v)
|
||||
}
|
||||
}
|
||||
59
argus/src/core/signals/shift_ops.rs
Normal file
59
argus/src/core/signals/shift_ops.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use core::iter::zip;
|
||||
use core::time::Duration;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{InterpolationMethod, Signal};
|
||||
|
||||
impl<T> Signal<T>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
/// Shift all samples in the signal by `delta` amount to the left.
|
||||
///
|
||||
/// This essentially filters out all samples with time points greater than `delta`,
|
||||
/// and subtracts `delta` from the rest of the time points.
|
||||
pub fn shift_left<I: InterpolationMethod<T>>(&self, delta: Duration) -> Self {
|
||||
match self {
|
||||
Signal::Sampled { values, time_points } => {
|
||||
// We want to skip any time points < delta, and subtract the rest.
|
||||
// Moreover, if the signal doesn't start at 0 after the shift, we may
|
||||
// want to interpolate from the previous value.
|
||||
|
||||
// Find the first index that satisfies `t >= delta` while also checking
|
||||
// if we need to interpolate
|
||||
let Some((idx, first_t)) = time_points.iter().find_position(|&t| t >= &delta) else {
|
||||
// Return an empty signal (we exhauseted all samples).
|
||||
return Signal::Empty;
|
||||
};
|
||||
|
||||
let mut new_samples: Vec<(Duration, T)> = Vec::with_capacity(time_points.len() - idx);
|
||||
// Let's see if we need to find a new sample
|
||||
if idx > 0 && first_t != &delta {
|
||||
// The shifted signal will not start at 0, and we have a previous
|
||||
// index to interpolate from.
|
||||
let v = self.interpolate_at::<I>(delta).unwrap();
|
||||
new_samples.push((Duration::ZERO, v));
|
||||
}
|
||||
// Shift the rest of the samples
|
||||
new_samples.extend(zip(&time_points[idx..], &values[idx..]).map(|(&t, &v)| (t - delta, v)));
|
||||
new_samples.into_iter().collect()
|
||||
}
|
||||
// Empty and constant signals can't really be changed
|
||||
sig => sig.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shift all samples in the signal by `delta` amount to the right.
|
||||
///
|
||||
/// This essentially adds `delta` to all time points.
|
||||
pub fn shift_right(&self, delta: Duration) -> Self {
|
||||
match self {
|
||||
Signal::Sampled { values, time_points } => {
|
||||
zip(time_points, values).map(|(&t, &v)| (t + delta, v)).collect()
|
||||
}
|
||||
// Empty and constant signals can't really be changed
|
||||
sig => sig.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
91
argus/src/core/signals/traits.rs
Normal file
91
argus/src/core/signals/traits.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
//! Helper traits for Argus signals.
|
||||
|
||||
use std::any::Any;
|
||||
use std::cmp::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use paste::paste;
|
||||
|
||||
use super::utils::Neighborhood;
|
||||
use super::{Sample, Signal};
|
||||
|
||||
/// Trait implemented by interpolation strategies
|
||||
pub trait InterpolationMethod<T> {
|
||||
/// Compute the interpolation of two samples at `time`.
|
||||
///
|
||||
/// Returns `None` if it isn't possible to interpolate at the given time using the
|
||||
/// given samples.
|
||||
fn at(a: &Sample<T>, b: &Sample<T>, time: Duration) -> Option<T>;
|
||||
|
||||
/// Given two signals with two sample points each, find the intersection of the two
|
||||
/// lines.
|
||||
fn find_intersection(a: &Neighborhood<T>, b: &Neighborhood<T>) -> Option<Sample<T>>;
|
||||
}
|
||||
|
||||
/// Simple trait to be used as a trait object for [`Signal<T>`] types.
|
||||
///
|
||||
/// This is mainly for external libraries to use for trait objects and downcasting to
|
||||
/// concrete [`Signal`] types.
|
||||
pub trait AnySignal {
|
||||
/// Convenience method to upcast a signal to [`std::any::Any`] for later downcasting
|
||||
/// to a concrete [`Signal`] type.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
impl<T: 'static> AnySignal for Signal<T> {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_signal_cmp {
|
||||
($cmp:ident) => {
|
||||
paste! {
|
||||
/// Compute the time-wise comparison of two signals
|
||||
fn [<signal_ $cmp>]<I>(&self, other: &Rhs) -> Option<Signal<bool>>
|
||||
where
|
||||
I: InterpolationMethod<T>
|
||||
{
|
||||
self.signal_cmp::<_, I>(other, |ord| ord.[<is_ $cmp>]())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A time-wise partial ordering defined for signals
|
||||
pub trait SignalPartialOrd<T, 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, I>(&self, other: &Rhs, op: F) -> Option<Signal<bool>>
|
||||
where
|
||||
F: Fn(Ordering) -> bool,
|
||||
I: InterpolationMethod<T>;
|
||||
|
||||
impl_signal_cmp!(lt);
|
||||
impl_signal_cmp!(le);
|
||||
impl_signal_cmp!(gt);
|
||||
impl_signal_cmp!(ge);
|
||||
impl_signal_cmp!(eq);
|
||||
impl_signal_cmp!(ne);
|
||||
}
|
||||
|
||||
/// Time-wise min-max of signal types
|
||||
pub trait SignalMinMax<Rhs = Self> {
|
||||
/// The output type of the signal after computing the min/max
|
||||
type Output;
|
||||
|
||||
/// Compute the time-wise min of two signals
|
||||
fn min(&self, rhs: &Rhs) -> Self::Output;
|
||||
|
||||
/// Compute the time-wise max of two signals
|
||||
fn max(&self, rhs: &Rhs) -> Self::Output;
|
||||
}
|
||||
|
||||
/// Trait for computing the absolute value of the samples in a signal
|
||||
pub trait SignalAbs {
|
||||
/// Compute the absolute value of the given signal
|
||||
fn abs(self) -> Self;
|
||||
}
|
||||
155
argus/src/core/signals/utils.rs
Normal file
155
argus/src/core/signals/utils.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
//! A bunch of utility code for argus
|
||||
//!
|
||||
//! - The implementation for Range intersection is based on the library
|
||||
//! [`range_ext`](https://github.com/AnickaBurova/range-ext), but adapted for my use a
|
||||
//! bit.
|
||||
|
||||
use core::ops::{Bound, RangeBounds};
|
||||
use core::time::Duration;
|
||||
|
||||
use super::{InterpolationMethod, Sample, Signal};
|
||||
|
||||
/// The neighborhood around a signal such that the time `at` is between the `first` and
|
||||
/// `second` samples.
|
||||
///
|
||||
/// The values of `first` and `second` are `None` if and only if `at` lies outside the
|
||||
/// domain over which the signal is defined.
|
||||
///
|
||||
/// This can be used to interpolate the value at the given `at` time using strategies
|
||||
/// like constant previous, constant following, and linear interpolation.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Neighborhood<T> {
|
||||
pub first: Option<Sample<T>>,
|
||||
pub second: Option<Sample<T>>,
|
||||
}
|
||||
|
||||
impl<T> Signal<T> {
|
||||
pub(crate) fn unary_op<U, F>(&self, op: F) -> Signal<U>
|
||||
where
|
||||
F: Fn(&T) -> U,
|
||||
Signal<U>: std::iter::FromIterator<(Duration, U)>,
|
||||
{
|
||||
use Signal::*;
|
||||
match self {
|
||||
Empty => Signal::Empty,
|
||||
Constant { value } => Signal::constant(op(value)),
|
||||
signal => signal.into_iter().map(|(&t, v)| (t, op(v))).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn binary_op<U, F, Interp>(&self, other: &Signal<T>, op: F) -> Signal<U>
|
||||
where
|
||||
T: Clone,
|
||||
F: Fn(&T, &T) -> U,
|
||||
Interp: InterpolationMethod<T>,
|
||||
{
|
||||
use Signal::*;
|
||||
match (self, other) {
|
||||
// If either of the signals are empty, we return an empty signal.
|
||||
(Empty, _) | (_, Empty) => Signal::Empty,
|
||||
(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::<Interp>(*t).unwrap();
|
||||
let v2 = rhs.interpolate_at::<Interp>(*t).unwrap();
|
||||
(*t, op(&v1, &v2))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn binary_op_with_intersection<U, F, Interp>(&self, other: &Signal<T>, op: F) -> Signal<U>
|
||||
where
|
||||
T: Clone + PartialOrd,
|
||||
F: Fn(&T, &T) -> U,
|
||||
Interp: InterpolationMethod<T>,
|
||||
{
|
||||
use Signal::*;
|
||||
match (self, other) {
|
||||
// If either of the signals are empty, we return an empty signal.
|
||||
(Empty, _) | (_, Empty) => Signal::Empty,
|
||||
(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_with_intersection::<Interp>(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::<Interp>(t).unwrap();
|
||||
let v2 = rhs.interpolate_at::<Interp>(t).unwrap();
|
||||
(t, op(&v1, &v2))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn partial_min<T>(a: T, b: T) -> Option<T>
|
||||
where
|
||||
T: PartialOrd,
|
||||
{
|
||||
a.partial_cmp(&b).map(|ord| if ord.is_lt() { a } else { b })
|
||||
}
|
||||
|
||||
fn partial_max<T>(a: T, b: T) -> Option<T>
|
||||
where
|
||||
T: PartialOrd,
|
||||
{
|
||||
a.partial_cmp(&b).map(|ord| if ord.is_gt() { a } else { b })
|
||||
}
|
||||
|
||||
/// Compute the intersection of two ranges
|
||||
pub fn intersect_bounds<T>(lhs: &impl RangeBounds<T>, rhs: &impl RangeBounds<T>) -> (Bound<T>, Bound<T>)
|
||||
where
|
||||
T: PartialOrd + Copy,
|
||||
{
|
||||
use core::ops::Bound::*;
|
||||
|
||||
let start = match (lhs.start_bound(), rhs.start_bound()) {
|
||||
(Included(&l), Included(&r)) => Included(partial_max(l, r).unwrap()),
|
||||
(Excluded(&l), Excluded(&r)) => Excluded(partial_max(l, r).unwrap()),
|
||||
|
||||
(Included(l), Excluded(r)) | (Excluded(r), Included(l)) => {
|
||||
if l > r {
|
||||
Included(*l)
|
||||
} else {
|
||||
Excluded(*r)
|
||||
}
|
||||
}
|
||||
|
||||
(Unbounded, Included(&l)) | (Included(&l), Unbounded) => Included(l),
|
||||
(Unbounded, Excluded(&l)) | (Excluded(&l), Unbounded) => Excluded(l),
|
||||
(Unbounded, Unbounded) => Unbounded,
|
||||
};
|
||||
|
||||
let end = match (lhs.end_bound(), rhs.end_bound()) {
|
||||
(Included(&l), Included(&r)) => Included(partial_min(l, r).unwrap()),
|
||||
(Excluded(&l), Excluded(&r)) => Excluded(partial_min(l, r).unwrap()),
|
||||
|
||||
(Included(l), Excluded(r)) | (Excluded(r), Included(l)) => {
|
||||
if l < r {
|
||||
Included(*l)
|
||||
} else {
|
||||
Excluded(*r)
|
||||
}
|
||||
}
|
||||
|
||||
(Unbounded, Included(&l)) | (Included(&l), Unbounded) => Included(l),
|
||||
(Unbounded, Excluded(&l)) | (Excluded(&l), Unbounded) => Excluded(l),
|
||||
(Unbounded, Unbounded) => Unbounded,
|
||||
};
|
||||
|
||||
(start, end)
|
||||
}
|
||||
103
argus/src/lib.rs
103
argus/src/lib.rs
|
|
@ -1,4 +1,99 @@
|
|||
pub use argus_core::signals::{AnySignal, Signal};
|
||||
pub use argus_core::{expr, signals, ArgusResult, Error};
|
||||
pub use argus_parser::parse_str;
|
||||
pub use argus_semantics::{BooleanSemantics, QuantitativeSemantics, Trace};
|
||||
#![warn(missing_docs)]
|
||||
#![doc = include_str!("../../README.md")]
|
||||
|
||||
extern crate self as argus;
|
||||
|
||||
mod core;
|
||||
pub mod parser;
|
||||
mod semantics;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::core::signals::{AnySignal, Signal};
|
||||
pub use crate::core::{expr, signals};
|
||||
pub use crate::parser::parse_str;
|
||||
pub use crate::semantics::{BooleanSemantics, QuantitativeSemantics, Trace};
|
||||
|
||||
/// Errors generated by all Argus components.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// An identifier has been redeclared in a specification.
|
||||
///
|
||||
/// This is called mainly from [`expr::ExprBuilder`].
|
||||
#[error("redeclaration of identifier")]
|
||||
IdentifierRedeclaration,
|
||||
|
||||
/// An expression is provided with an insufficient number of arguments.
|
||||
///
|
||||
/// This is called for N-ary expressions:
|
||||
/// [`NumExpr::Add`](crate::expr::NumExpr::Add),
|
||||
/// [`NumExpr::Mul`](crate::expr::NumExpr::Mul),
|
||||
/// [`BoolExpr::And`](crate::expr::BoolExpr::And), and
|
||||
/// [`BoolExpr::Or`](crate::expr::BoolExpr::Or).
|
||||
#[error("insufficient number of arguments")]
|
||||
IncompleteArgs,
|
||||
|
||||
/// Attempting to `push` a new sample to a non-sampled signal
|
||||
/// ([`Signal::Empty`](crate::signals::Signal::Empty) or
|
||||
/// [`Signal::Constant`](crate::signals::Signal::Constant)).
|
||||
#[error("cannot push value to non-sampled signal")]
|
||||
InvalidPushToSignal,
|
||||
|
||||
/// Pushing the new value to the sampled signal makes it not strictly monotonically
|
||||
/// increasing.
|
||||
#[error(
|
||||
"trying to create a non-monotonically signal, signal end time ({end_time:?}) > sample time point \
|
||||
({current_sample:?})"
|
||||
)]
|
||||
NonMonotonicSignal {
|
||||
/// The time that the signal actually ends
|
||||
end_time: Duration,
|
||||
/// The time point of the new (erroneous) sample.
|
||||
current_sample: Duration,
|
||||
},
|
||||
|
||||
/// Attempting to perform an invalid operation on a signal
|
||||
#[error("invalid operation on signal")]
|
||||
InvalidOperation,
|
||||
|
||||
/// Attempting to index a signal not present in a trace.
|
||||
#[error("name not in signal trace")]
|
||||
SignalNotPresent,
|
||||
|
||||
/// Attempting to perform a signal operation not supported by the type
|
||||
#[error("incorrect signal type")]
|
||||
InvalidSignalType,
|
||||
|
||||
/// Incorrect cast of signal
|
||||
#[error("invalid cast from {from} to {to}")]
|
||||
InvalidCast {
|
||||
/// Type of the signal being cast from
|
||||
from: &'static str,
|
||||
/// Type of the signal being cast to
|
||||
to: &'static str,
|
||||
},
|
||||
|
||||
/// Invalid interval
|
||||
#[error("invalid interval: {reason}")]
|
||||
InvalidInterval {
|
||||
/// Reason for interval being invalid
|
||||
reason: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// An [`InvalidCast`](Error::InvalidCast) error from `T` to `U`.
|
||||
pub fn invalid_cast<T, U>() -> Self {
|
||||
Self::InvalidCast {
|
||||
from: std::any::type_name::<T>(),
|
||||
to: std::any::type_name::<U>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for [`Error`](enum@Error)
|
||||
pub type ArgusError = Error;
|
||||
/// Alias for [`Result<T, ArgusError>`]
|
||||
pub type ArgusResult<T> = Result<T, Error>;
|
||||
|
|
|
|||
233
argus/src/parser/lexer.rs
Normal file
233
argus/src/parser/lexer.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
use std::fmt;
|
||||
|
||||
use chumsky::prelude::*;
|
||||
|
||||
pub(crate) type Span = SimpleSpan<usize>;
|
||||
pub(crate) type Output<'a> = Vec<(Token<'a>, Span)>;
|
||||
pub(crate) type Error<'a> = extra::Err<Rich<'a, char, Span>>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Token<'src> {
|
||||
Semicolon,
|
||||
LBracket,
|
||||
RBracket,
|
||||
LParen,
|
||||
RParen,
|
||||
Comma,
|
||||
DotDot,
|
||||
Bool(bool),
|
||||
Int(i64),
|
||||
UInt(u64),
|
||||
Float(f64),
|
||||
Ident(&'src str),
|
||||
Minus,
|
||||
Plus,
|
||||
Times,
|
||||
Divide,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
Eq,
|
||||
Neq,
|
||||
Assign,
|
||||
Not,
|
||||
And,
|
||||
Or,
|
||||
Implies,
|
||||
Equiv,
|
||||
Xor,
|
||||
Next,
|
||||
Always,
|
||||
Eventually,
|
||||
Until,
|
||||
}
|
||||
|
||||
impl<'src> fmt::Display for Token<'src> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Token::Semicolon => write!(f, ";"),
|
||||
Token::LBracket => write!(f, "["),
|
||||
Token::RBracket => write!(f, "]"),
|
||||
Token::LParen => write!(f, "("),
|
||||
Token::RParen => write!(f, ")"),
|
||||
Token::Comma => write!(f, ","),
|
||||
Token::DotDot => write!(f, ".."),
|
||||
Token::Bool(val) => write!(f, "{}", val),
|
||||
Token::Int(val) => write!(f, "{}", val),
|
||||
Token::UInt(val) => write!(f, "{}", val),
|
||||
Token::Float(val) => write!(f, "{}", val),
|
||||
Token::Ident(ident) => write!(f, "{}", ident),
|
||||
Token::Minus => write!(f, "-"),
|
||||
Token::Plus => write!(f, "+"),
|
||||
Token::Times => write!(f, "*"),
|
||||
Token::Divide => write!(f, "/"),
|
||||
Token::Lt => write!(f, "<"),
|
||||
Token::Le => write!(f, "<="),
|
||||
Token::Gt => write!(f, ">"),
|
||||
Token::Ge => write!(f, ">="),
|
||||
Token::Eq => write!(f, "=="),
|
||||
Token::Neq => write!(f, "!="),
|
||||
Token::Assign => write!(f, "="),
|
||||
Token::Not => write!(f, "!"),
|
||||
Token::And => write!(f, "&"),
|
||||
Token::Or => write!(f, "|"),
|
||||
Token::Implies => write!(f, "->"),
|
||||
Token::Equiv => write!(f, "<->"),
|
||||
Token::Xor => write!(f, "^"),
|
||||
Token::Next => write!(f, "X"),
|
||||
Token::Always => write!(f, "G"),
|
||||
Token::Eventually => write!(f, "F"),
|
||||
Token::Until => write!(f, "U"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lexer<'src>() -> impl Parser<'src, &'src str, Output<'src>, Error<'src>> {
|
||||
// A parser for numbers
|
||||
let digits = text::digits(10).slice();
|
||||
|
||||
let frac = just('.').then(digits);
|
||||
|
||||
let exp = just('e').or(just('E')).then(one_of("+-").or_not()).then(digits);
|
||||
|
||||
let floating_number = just('-')
|
||||
.or_not()
|
||||
.then(digits)
|
||||
.then(choice((frac.then(exp).slice(), frac.slice(), exp.slice())))
|
||||
// .then(frac.or_not())
|
||||
// .then(exp.or_not())
|
||||
.map_slice(|s: &str| Token::Float(s.parse().unwrap()))
|
||||
.boxed();
|
||||
|
||||
let signed_int = one_of("+-")
|
||||
// .or_not()
|
||||
.then(digits)
|
||||
.then(frac.not().or(exp.not()))
|
||||
.map_slice(|s: &str| Token::Int(s.parse().unwrap()));
|
||||
let unsigned_int = digits
|
||||
.then(frac.not().or(exp.not()))
|
||||
.map_slice(|s: &str| Token::UInt(s.parse().unwrap()));
|
||||
|
||||
let number = choice((floating_number, signed_int, unsigned_int));
|
||||
|
||||
// A parser for control characters (delimiters, semicolons, etc.)
|
||||
let ctrl = choice((
|
||||
just(";").to(Token::Semicolon),
|
||||
just("[").to(Token::LBracket),
|
||||
just("]").to(Token::RBracket),
|
||||
just("(").to(Token::LParen),
|
||||
just(")").to(Token::RParen),
|
||||
just(",").to(Token::Comma),
|
||||
just("..").to(Token::DotDot),
|
||||
));
|
||||
|
||||
// Lexer for operator symbols
|
||||
let op = choice((
|
||||
just("<->").to(Token::Equiv),
|
||||
just("<=>").to(Token::Equiv),
|
||||
just("<=").to(Token::Le),
|
||||
just("<").to(Token::Lt),
|
||||
just(">=").to(Token::Ge),
|
||||
just(">").to(Token::Gt),
|
||||
just("!=").to(Token::Neq),
|
||||
just("==").to(Token::Eq),
|
||||
just("->").to(Token::Implies),
|
||||
just("=>").to(Token::Implies),
|
||||
just("!").to(Token::Not),
|
||||
just("~").to(Token::Not),
|
||||
just("\u{00ac}").to(Token::Not), // ¬
|
||||
just("&&").to(Token::And),
|
||||
just("&").to(Token::And),
|
||||
just("\u{2227}").to(Token::And), // ∧
|
||||
just("||").to(Token::And),
|
||||
just("|").to(Token::And),
|
||||
just("\u{2228}").to(Token::Or), // ∨
|
||||
just("^").to(Token::Xor),
|
||||
just("-").to(Token::Minus),
|
||||
just("+").to(Token::Plus),
|
||||
just("*").to(Token::Times),
|
||||
just("/").to(Token::Divide),
|
||||
just("=").to(Token::Assign),
|
||||
));
|
||||
|
||||
let temporal_op = choice((
|
||||
just("\u{25cb}").to(Token::Next), // ○
|
||||
just("\u{25ef}").to(Token::Next), // ◯
|
||||
just("\u{25c7}").to(Token::Eventually), // ◇
|
||||
just("\u{25a1}").to(Token::Always), // □
|
||||
));
|
||||
|
||||
// A parser for strings
|
||||
// Strings in our grammar are identifiers too
|
||||
let quoted_ident = just('"')
|
||||
.ignore_then(none_of('"').repeated())
|
||||
.then_ignore(just('"'))
|
||||
.map_slice(Token::Ident);
|
||||
|
||||
// A parser for identifiers and keywords
|
||||
let ident = text::ident().map(|ident: &str| match ident {
|
||||
"true" => Token::Bool(true),
|
||||
"false" => Token::Bool(false),
|
||||
"TRUE" => Token::Bool(true),
|
||||
"FALSE" => Token::Bool(false),
|
||||
"G" => Token::Always,
|
||||
"alw" => Token::Always,
|
||||
"always" => Token::Always,
|
||||
"globally" => Token::Always,
|
||||
"F" => Token::Eventually,
|
||||
"ev" => Token::Eventually,
|
||||
"eventually" => Token::Eventually,
|
||||
"finally" => Token::Eventually,
|
||||
"X" => Token::Next,
|
||||
"next" => Token::Next,
|
||||
"U" => Token::Until,
|
||||
"until" => Token::Until,
|
||||
_ => Token::Ident(ident),
|
||||
});
|
||||
|
||||
// A single token can be one of the above
|
||||
let token = choice((op, temporal_op, ctrl, quoted_ident, ident, number)).boxed();
|
||||
|
||||
let comment = just("//").then(any().and_is(just('\n').not()).repeated()).padded();
|
||||
|
||||
token
|
||||
.map_with_span(|tok, span| (tok, span))
|
||||
.padded_by(comment.repeated())
|
||||
.padded()
|
||||
// If we encounter an error, skip and attempt to lex the next character as a token instead
|
||||
.recover_with(skip_then_retry_until(any().ignored(), end()))
|
||||
.repeated()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
use Token::*;
|
||||
let cases = [
|
||||
("true", vec![(Bool(true), Span::new(0, 4))]),
|
||||
("false", vec![(Bool(false), Span::new(0, 5))]),
|
||||
(
|
||||
"F a",
|
||||
vec![(Eventually, Span::new(0, 1)), (Ident("a"), Span::new(2, 3))],
|
||||
),
|
||||
(
|
||||
"a U b",
|
||||
vec![
|
||||
(Ident("a"), Span::new(0, 1)),
|
||||
(Until, Span::new(2, 3)),
|
||||
(Ident("b"), Span::new(4, 5)),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected) in cases {
|
||||
let actual = lexer().parse(input).into_result().unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
306
argus/src/parser/mod.rs
Normal file
306
argus/src/parser/mod.rs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
//! # crate::core logic syntax
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::core::expr::ExprBuilder;
|
||||
mod lexer;
|
||||
mod syntax;
|
||||
|
||||
use chumsky::prelude::Rich;
|
||||
use lexer::{lexer, Token};
|
||||
use syntax::{parser, Expr, Interval};
|
||||
|
||||
/// Parse a string expression into a concrete Argus expression.
|
||||
pub fn parse_str(src: &str) -> Result<crate::core::expr::Expr, Vec<Rich<'_, String>>> {
|
||||
use chumsky::prelude::{Input, Parser};
|
||||
|
||||
let (tokens, lex_errors) = lexer().parse(src).into_output_errors();
|
||||
|
||||
let (parsed, parse_errors) = if let Some(tokens) = &tokens {
|
||||
parser()
|
||||
.parse(tokens.as_slice().spanned((src.len()..src.len()).into()))
|
||||
.into_output_errors()
|
||||
} else {
|
||||
(None, Vec::new())
|
||||
};
|
||||
|
||||
let (expr, expr_errors) = if let Some((ast, span)) = parsed {
|
||||
let mut expr_builder = ExprBuilder::new();
|
||||
let result = ast_to_expr(&ast, span, &mut expr_builder);
|
||||
match result {
|
||||
Ok(expr) => (Some(expr), vec![]),
|
||||
Err(err) => (None, vec![err]),
|
||||
}
|
||||
} else {
|
||||
(None, vec![])
|
||||
};
|
||||
|
||||
let errors: Vec<_> = lex_errors
|
||||
.into_iter()
|
||||
.map(|e| e.map_token(|c| c.to_string()))
|
||||
.chain(parse_errors.into_iter().map(|e| e.map_token(|tok| tok.to_string())))
|
||||
.chain(expr_errors.into_iter().map(|e| e.map_token(|tok| tok.to_string())))
|
||||
.map(|e| e.into_owned())
|
||||
.collect();
|
||||
if !errors.is_empty() {
|
||||
Err(errors)
|
||||
} else {
|
||||
expr.ok_or(errors)
|
||||
}
|
||||
}
|
||||
|
||||
fn interval_convert(interval: &Interval<'_>) -> crate::core::expr::Interval {
|
||||
use core::ops::Bound;
|
||||
let a = if let Some(a) = &interval.a {
|
||||
match &a.0 {
|
||||
Expr::UInt(value) => Bound::Included(Duration::from_secs(*value)),
|
||||
Expr::Float(value) => Bound::Included(Duration::from_secs_f64(*value)),
|
||||
_ => unreachable!("must be valid numeric literal."),
|
||||
}
|
||||
} else {
|
||||
Bound::Unbounded
|
||||
};
|
||||
let b = if let Some(b) = &interval.b {
|
||||
match &b.0 {
|
||||
Expr::UInt(value) => Bound::Included(Duration::from_secs(*value)),
|
||||
Expr::Float(value) => Bound::Included(Duration::from_secs_f64(*value)),
|
||||
_ => unreachable!("must be valid numeric literal."),
|
||||
}
|
||||
} else {
|
||||
Bound::Unbounded
|
||||
};
|
||||
crate::core::expr::Interval::new(a, b)
|
||||
}
|
||||
|
||||
/// Convert a parsed [`Expr`] into an [crate::core `Expr`](crate::core::expr::Expr)
|
||||
fn ast_to_expr<'tokens, 'src: 'tokens>(
|
||||
ast: &Expr<'src>,
|
||||
span: lexer::Span,
|
||||
ctx: &mut ExprBuilder,
|
||||
) -> Result<crate::core::expr::Expr, Rich<'tokens, Token<'src>, lexer::Span>> {
|
||||
match ast {
|
||||
Expr::Error => unreachable!("Errors should have been caught by parser"),
|
||||
Expr::Bool(value) => Ok(ctx.bool_const(*value).into()),
|
||||
Expr::Int(value) => Ok(ctx.int_const(*value).into()),
|
||||
Expr::UInt(value) => Ok(ctx.uint_const(*value).into()),
|
||||
Expr::Float(value) => Ok(ctx.float_const(*value).into()),
|
||||
Expr::Var { name, kind } => match kind {
|
||||
syntax::Type::Unknown => Err(Rich::custom(span, "All variables must have defined type by now.")),
|
||||
syntax::Type::Bool => ctx
|
||||
.bool_var(name.to_string())
|
||||
.map(|var| var.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string())),
|
||||
syntax::Type::UInt => ctx
|
||||
.uint_var(name.to_string())
|
||||
.map(|var| var.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string())),
|
||||
syntax::Type::Int => ctx
|
||||
.int_var(name.to_string())
|
||||
.map(|var| var.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string())),
|
||||
syntax::Type::Float => ctx
|
||||
.float_var(name.to_string())
|
||||
.map(|var| var.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string())),
|
||||
},
|
||||
Expr::Unary { op, interval, arg } => {
|
||||
let arg = ast_to_expr(&arg.0, arg.1, ctx)?;
|
||||
let interval = interval.as_ref().map(|(i, span)| (interval_convert(i), span));
|
||||
match op {
|
||||
syntax::UnaryOps::Neg => {
|
||||
assert!(interval.is_none());
|
||||
let crate::core::expr::Expr::Num(arg) = arg else {
|
||||
unreachable!("- must have numeric expression argument");
|
||||
};
|
||||
Ok(ctx.make_neg(Box::new(arg)).into())
|
||||
}
|
||||
syntax::UnaryOps::Not => {
|
||||
assert!(interval.is_none());
|
||||
let crate::core::expr::Expr::Bool(arg) = arg else {
|
||||
unreachable!("`Not` must have boolean expression argument");
|
||||
};
|
||||
Ok(ctx.make_not(Box::new(arg)).into())
|
||||
}
|
||||
syntax::UnaryOps::Next => {
|
||||
use core::ops::Bound;
|
||||
let crate::core::expr::Expr::Bool(arg) = arg else {
|
||||
unreachable!("`Next` must have boolean expression argument");
|
||||
};
|
||||
match interval {
|
||||
Some((interval, ispan)) => {
|
||||
let steps: usize = match (interval.start, interval.end) {
|
||||
(Bound::Included(start), Bound::Included(end)) => (end - start).as_secs() as usize,
|
||||
_ => {
|
||||
return Err(Rich::custom(
|
||||
*ispan,
|
||||
"Oracle operation (X[..]) cannot have unbounded intervals",
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(ctx.make_oracle(steps, Box::new(arg)).into())
|
||||
}
|
||||
None => Ok(ctx.make_next(Box::new(arg)).into()),
|
||||
}
|
||||
}
|
||||
syntax::UnaryOps::Always => {
|
||||
let crate::core::expr::Expr::Bool(arg) = arg else {
|
||||
unreachable!("`Always` must have boolean expression argument");
|
||||
};
|
||||
match interval {
|
||||
Some((interval, _)) => Ok(ctx.make_timed_always(interval, Box::new(arg)).into()),
|
||||
None => Ok(ctx.make_always(Box::new(arg)).into()),
|
||||
}
|
||||
}
|
||||
syntax::UnaryOps::Eventually => {
|
||||
let crate::core::expr::Expr::Bool(arg) = arg else {
|
||||
unreachable!("`Eventually` must have boolean expression argument");
|
||||
};
|
||||
match interval {
|
||||
Some((interval, _)) => Ok(ctx.make_timed_eventually(interval, Box::new(arg)).into()),
|
||||
None => Ok(ctx.make_eventually(Box::new(arg)).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Binary {
|
||||
op,
|
||||
interval,
|
||||
args: (lhs, rhs),
|
||||
} => {
|
||||
let lhs = ast_to_expr(&lhs.0, lhs.1, ctx)?;
|
||||
let rhs = ast_to_expr(&rhs.0, rhs.1, ctx)?;
|
||||
let interval = interval.as_ref().map(|(i, span)| (interval_convert(i), span));
|
||||
|
||||
match op {
|
||||
syntax::BinaryOps::Add => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Add` must have numeric expression arguments");
|
||||
};
|
||||
ctx.make_add([lhs, rhs])
|
||||
.map(|ex| ex.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string()))
|
||||
}
|
||||
syntax::BinaryOps::Sub => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Sub` must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_sub(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::Mul => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Mul` must have numeric expression arguments");
|
||||
};
|
||||
ctx.make_mul([lhs, rhs])
|
||||
.map(|ex| ex.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string()))
|
||||
}
|
||||
syntax::BinaryOps::Div => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Div` must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_div(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::Lt => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("Relational operation must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_lt(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::Le => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("Relational operation must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_le(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::Gt => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("Relational operation must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_gt(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::Ge => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("Relational operation must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_ge(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::Eq => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("Relational operation must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_eq(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::Neq => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Num(lhs), crate::core::expr::Expr::Num(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("Relational operation must have numeric expression arguments");
|
||||
};
|
||||
Ok(ctx.make_neq(Box::new(lhs), Box::new(rhs)).into())
|
||||
}
|
||||
syntax::BinaryOps::And => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Bool(lhs), crate::core::expr::Expr::Bool(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`And` must have boolean expression arguments");
|
||||
};
|
||||
ctx.make_and([lhs, rhs])
|
||||
.map(|ex| ex.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string()))
|
||||
}
|
||||
syntax::BinaryOps::Or => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Bool(lhs), crate::core::expr::Expr::Bool(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Or` must have boolean expression arguments");
|
||||
};
|
||||
ctx.make_or([lhs, rhs])
|
||||
.map(|ex| ex.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string()))
|
||||
}
|
||||
syntax::BinaryOps::Implies => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Bool(lhs), crate::core::expr::Expr::Bool(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Implies` must have boolean expression arguments");
|
||||
};
|
||||
ctx.make_implies(Box::new(lhs), Box::new(rhs))
|
||||
.map(|ex| ex.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string()))
|
||||
}
|
||||
syntax::BinaryOps::Equiv => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Bool(lhs), crate::core::expr::Expr::Bool(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Equiv` must have boolean expression arguments");
|
||||
};
|
||||
ctx.make_equiv(Box::new(lhs), Box::new(rhs))
|
||||
.map(|ex| ex.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string()))
|
||||
}
|
||||
syntax::BinaryOps::Xor => {
|
||||
assert!(interval.is_none());
|
||||
let (crate::core::expr::Expr::Bool(lhs), crate::core::expr::Expr::Bool(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Xor` must have boolean expression arguments");
|
||||
};
|
||||
ctx.make_xor(Box::new(lhs), Box::new(rhs))
|
||||
.map(|ex| ex.into())
|
||||
.map_err(|err| Rich::custom(span, err.to_string()))
|
||||
}
|
||||
syntax::BinaryOps::Until => {
|
||||
let (crate::core::expr::Expr::Bool(lhs), crate::core::expr::Expr::Bool(rhs)) = (lhs, rhs) else {
|
||||
unreachable!("`Until` must have boolean expression arguments");
|
||||
};
|
||||
match interval {
|
||||
Some((interval, _)) => Ok(ctx.make_timed_until(interval, Box::new(lhs), Box::new(rhs)).into()),
|
||||
None => Ok(ctx.make_until(Box::new(lhs), Box::new(rhs)).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
419
argus/src/parser/syntax.rs
Normal file
419
argus/src/parser/syntax.rs
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
use chumsky::input::SpannedInput;
|
||||
use chumsky::prelude::*;
|
||||
use chumsky::Parser;
|
||||
|
||||
use super::lexer::{Span, Token};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
#[default]
|
||||
Unknown,
|
||||
Bool,
|
||||
UInt,
|
||||
Int,
|
||||
Float,
|
||||
}
|
||||
|
||||
impl Type {
|
||||
/// Get the lowest common supertype for the given types to a common
|
||||
fn get_common_cast(self, other: Self) -> Self {
|
||||
use Type::*;
|
||||
match (self, other) {
|
||||
(Unknown, other) | (other, Unknown) => other,
|
||||
(Bool, ty) | (ty, Bool) => ty,
|
||||
(UInt, Int) => Float,
|
||||
(UInt, Float) => Float,
|
||||
(Int, UInt) => Float,
|
||||
(Int, Float) => Float,
|
||||
(Float, UInt) => Float,
|
||||
(Float, Int) => Float,
|
||||
(lhs, rhs) => {
|
||||
assert_eq!(lhs, rhs);
|
||||
rhs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Interval<'src> {
|
||||
pub a: Option<Box<Spanned<Expr<'src>>>>,
|
||||
pub b: Option<Box<Spanned<Expr<'src>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UnaryOps {
|
||||
Neg,
|
||||
Not,
|
||||
Next,
|
||||
Always,
|
||||
Eventually,
|
||||
}
|
||||
|
||||
impl UnaryOps {
|
||||
fn default_args_type(&self) -> Type {
|
||||
match self {
|
||||
UnaryOps::Neg => Type::Float,
|
||||
_ => Type::Bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BinaryOps {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
Eq,
|
||||
Neq,
|
||||
And,
|
||||
Or,
|
||||
Implies,
|
||||
Xor,
|
||||
Equiv,
|
||||
Until,
|
||||
}
|
||||
|
||||
impl BinaryOps {
|
||||
fn default_args_type(&self) -> Type {
|
||||
match self {
|
||||
BinaryOps::Add
|
||||
| BinaryOps::Sub
|
||||
| BinaryOps::Mul
|
||||
| BinaryOps::Div
|
||||
| BinaryOps::Lt
|
||||
| BinaryOps::Le
|
||||
| BinaryOps::Gt
|
||||
| BinaryOps::Ge
|
||||
| BinaryOps::Eq
|
||||
| BinaryOps::Neq => Type::Float,
|
||||
_ => Type::Bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Expr<'src> {
|
||||
Error,
|
||||
Bool(bool),
|
||||
Int(i64),
|
||||
UInt(u64),
|
||||
Float(f64),
|
||||
Var {
|
||||
name: &'src str,
|
||||
kind: Type,
|
||||
},
|
||||
Unary {
|
||||
op: UnaryOps,
|
||||
interval: Option<Spanned<Interval<'src>>>,
|
||||
arg: Box<Spanned<Self>>,
|
||||
},
|
||||
Binary {
|
||||
op: BinaryOps,
|
||||
interval: Option<Spanned<Interval<'src>>>,
|
||||
args: (Box<Spanned<Self>>, Box<Spanned<Self>>),
|
||||
},
|
||||
}
|
||||
|
||||
impl<'src> Expr<'src> {
|
||||
fn get_type(&self) -> Type {
|
||||
match self {
|
||||
Expr::Error => Type::Unknown,
|
||||
Expr::Bool(_) => Type::Bool,
|
||||
Expr::Int(_) => Type::Int,
|
||||
Expr::UInt(_) => Type::UInt,
|
||||
Expr::Float(_) => Type::Float,
|
||||
Expr::Var { name: _, kind } => *kind,
|
||||
Expr::Unary {
|
||||
op,
|
||||
interval: _,
|
||||
arg: _,
|
||||
} => op.default_args_type(),
|
||||
Expr::Binary {
|
||||
op,
|
||||
interval: _,
|
||||
args: _,
|
||||
} => op.default_args_type(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make untyped (`Type::Unknown`) expressions into the given type.
|
||||
fn make_typed(mut self, ty: Type) -> Self {
|
||||
if let Expr::Var { name: _, kind } = &mut self {
|
||||
*kind = ty;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn unary_op(op: UnaryOps, arg: Spanned<Self>, interval: Option<Spanned<Interval<'src>>>) -> Self {
|
||||
let arg = Box::new((arg.0.make_typed(op.default_args_type()), arg.1));
|
||||
Self::Unary { op, interval, arg }
|
||||
}
|
||||
|
||||
fn binary_op(
|
||||
op: BinaryOps,
|
||||
args: (Spanned<Self>, Spanned<Self>),
|
||||
interval: Option<Spanned<Interval<'src>>>,
|
||||
) -> Self {
|
||||
let (lhs, lspan) = args.0;
|
||||
let (rhs, rspan) = args.1;
|
||||
|
||||
let common_type = lhs.get_type().get_common_cast(rhs.get_type());
|
||||
let common_type = if Type::Unknown == common_type {
|
||||
op.default_args_type()
|
||||
} else {
|
||||
common_type
|
||||
};
|
||||
let lhs = Box::new((lhs.make_typed(common_type), lspan));
|
||||
let rhs = Box::new((rhs.make_typed(common_type), rspan));
|
||||
|
||||
Self::Binary {
|
||||
op,
|
||||
interval,
|
||||
args: (lhs, rhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Spanned<T> = (T, Span);
|
||||
|
||||
pub type Error<'tokens, 'src> = extra::Err<Rich<'tokens, Token<'src>, Span>>;
|
||||
|
||||
// The type of the input that our parser operates on. The input is the `&[(Token,
|
||||
// Span)]` token buffer generated by the lexer, wrapped in a `SpannedInput` which
|
||||
// 'splits' it apart into its constituent parts, tokens and spans, for chumsky
|
||||
// to understand.
|
||||
type ParserInput<'tokens, 'src> = SpannedInput<Token<'src>, Span, &'tokens [(Token<'src>, Span)]>;
|
||||
|
||||
fn num_expr_parser<'tokens, 'src: 'tokens>(
|
||||
) -> impl Parser<'tokens, ParserInput<'tokens, 'src>, Spanned<Expr<'src>>, Error<'tokens, 'src>> + Clone {
|
||||
recursive(|num_expr| {
|
||||
let var = select! { Token::Ident(name) => Expr::Var{ name, kind: Type::default()} }.labelled("variable");
|
||||
|
||||
let num_literal = select! {
|
||||
Token::Int(val) => Expr::Int(val),
|
||||
Token::UInt(val) => Expr::UInt(val),
|
||||
Token::Float(val) => Expr::Float(val),
|
||||
}
|
||||
.labelled("number literal");
|
||||
|
||||
let num_atom = var
|
||||
.or(num_literal)
|
||||
.map_with_span(|expr, span: Span| (expr, span))
|
||||
// Atoms can also just be normal expressions, but surrounded with parentheses
|
||||
.or(num_expr.clone().delimited_by(just(Token::LParen), just(Token::RParen)))
|
||||
// Attempt to recover anything that looks like a parenthesised expression but contains errors
|
||||
.recover_with(via_parser(nested_delimiters(
|
||||
Token::LParen,
|
||||
Token::RParen,
|
||||
[(Token::LBracket, Token::RBracket)],
|
||||
|span| (Expr::Error, span),
|
||||
)))
|
||||
.boxed();
|
||||
|
||||
let neg_op = just(Token::Minus)
|
||||
.map_with_span(|_, span: Span| (UnaryOps::Neg, span))
|
||||
.repeated()
|
||||
.foldr(num_atom, |op, rhs| {
|
||||
let span = op.1.start..rhs.1.end;
|
||||
(Expr::unary_op(op.0, rhs, None), span.into())
|
||||
});
|
||||
|
||||
// Product ops (multiply and divide) have equal precedence
|
||||
let product_op = {
|
||||
let op = choice((
|
||||
just(Token::Times).to(BinaryOps::Mul),
|
||||
just(Token::Divide).to(BinaryOps::Div),
|
||||
));
|
||||
neg_op
|
||||
.clone()
|
||||
.foldl(op.then(neg_op).repeated(), |a, (op, b)| {
|
||||
let span = a.1.start..b.1.end;
|
||||
(Expr::binary_op(op, (a, b), None), span.into())
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
|
||||
// Sum ops (add and subtract) have equal precedence
|
||||
let sum_op = {
|
||||
let op = choice((
|
||||
just(Token::Plus).to(BinaryOps::Add),
|
||||
just(Token::Minus).to(BinaryOps::Sub),
|
||||
));
|
||||
product_op
|
||||
.clone()
|
||||
.foldl(op.then(product_op).repeated(), |a, (op, b)| {
|
||||
let span = a.1.start..b.1.end;
|
||||
(Expr::binary_op(op, (a, b), None), span.into())
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
|
||||
sum_op.labelled("numeric expression").as_context()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parser<'tokens, 'src: 'tokens>(
|
||||
) -> impl Parser<'tokens, ParserInput<'tokens, 'src>, Spanned<Expr<'src>>, Error<'tokens, 'src>> + Clone {
|
||||
let interval = {
|
||||
let num_literal = select! {
|
||||
Token::UInt(val) => Expr::UInt(val),
|
||||
Token::Float(val) => Expr::Float(val),
|
||||
}
|
||||
.map_with_span(|lit, span: Span| (lit, span));
|
||||
let sep = just(Token::Comma).or(just(Token::DotDot));
|
||||
|
||||
num_literal
|
||||
.or_not()
|
||||
.then_ignore(sep)
|
||||
.then(num_literal.or_not())
|
||||
.delimited_by(just(Token::LBracket), just(Token::RBracket))
|
||||
.map_with_span(|(a, b), span| {
|
||||
(
|
||||
Interval {
|
||||
a: a.map(Box::new),
|
||||
b: b.map(Box::new),
|
||||
},
|
||||
span,
|
||||
)
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
let num_expr = num_expr_parser().boxed();
|
||||
|
||||
recursive(|expr| {
|
||||
let literal = select! {
|
||||
Token::Bool(val) => Expr::Bool(val),
|
||||
}
|
||||
.labelled("boolean literal");
|
||||
|
||||
let var =
|
||||
select! { Token::Ident(ident) => Expr::Var{ name: ident, kind: Type::default()} }.labelled("variable");
|
||||
|
||||
// Relational ops (<, <=, >, >=, ==, !=) have equal precedence
|
||||
let relational_op = {
|
||||
let op = choice((
|
||||
just(Token::Lt).to(BinaryOps::Lt),
|
||||
just(Token::Le).to(BinaryOps::Le),
|
||||
just(Token::Gt).to(BinaryOps::Gt),
|
||||
just(Token::Ge).to(BinaryOps::Ge),
|
||||
just(Token::Eq).to(BinaryOps::Eq),
|
||||
just(Token::Neq).to(BinaryOps::Neq),
|
||||
));
|
||||
num_expr
|
||||
.clone()
|
||||
.then(op.then(num_expr))
|
||||
.map(|(a, (op, b))| {
|
||||
let span = a.1.start..b.1.end;
|
||||
(Expr::binary_op(op, (a, b), None), span.into())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
.labelled("atomic predicate");
|
||||
|
||||
let atom = relational_op
|
||||
.or(var.or(literal).map_with_span(|expr, span| (expr, span)))
|
||||
// Atoms can also just be normal expressions, but surrounded with parentheses
|
||||
.or(expr.clone().delimited_by(just(Token::LParen), just(Token::RParen)))
|
||||
// Attempt to recover anything that looks like a parenthesised expression but contains errors
|
||||
.recover_with(via_parser(nested_delimiters(
|
||||
Token::LParen,
|
||||
Token::RParen,
|
||||
[],
|
||||
|span| (Expr::Error, span),
|
||||
)))
|
||||
.boxed();
|
||||
|
||||
let not_op = just(Token::Not)
|
||||
.map_with_span(|_, span: Span| (UnaryOps::Not, span))
|
||||
.repeated()
|
||||
.foldr(atom, |op, rhs| {
|
||||
let span = op.1.start..rhs.1.end;
|
||||
(Expr::unary_op(op.0, rhs, None), span.into())
|
||||
})
|
||||
.boxed();
|
||||
|
||||
let unary_temporal_op = {
|
||||
let op = choice((
|
||||
just(Token::Next).to(UnaryOps::Next),
|
||||
just(Token::Eventually).to(UnaryOps::Eventually),
|
||||
just(Token::Always).to(UnaryOps::Always),
|
||||
));
|
||||
op.map_with_span(|op, span: Span| (op, span))
|
||||
.then(interval.clone().or_not())
|
||||
.repeated()
|
||||
.foldr(not_op, |(op, interval), rhs| {
|
||||
let span = op.1.start..rhs.1.end;
|
||||
(Expr::unary_op(op.0, rhs, interval), span.into())
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
|
||||
let binary_temporal_op = unary_temporal_op
|
||||
.clone()
|
||||
.then(just(Token::Until).to(BinaryOps::Until).then(interval.or_not()))
|
||||
.repeated()
|
||||
.foldr(unary_temporal_op, |(lhs, (op, interval)), rhs| {
|
||||
let span = lhs.1.start..rhs.1.end;
|
||||
assert_eq!(op, BinaryOps::Until);
|
||||
(Expr::binary_op(op, (lhs, rhs), interval), span.into())
|
||||
})
|
||||
.boxed();
|
||||
|
||||
let and_op = {
|
||||
let op = just(Token::And).to(BinaryOps::And);
|
||||
binary_temporal_op
|
||||
.clone()
|
||||
.foldl(op.then(binary_temporal_op).repeated(), |a, (op, b)| {
|
||||
let span = a.1.start..b.1.end;
|
||||
(Expr::binary_op(op, (a, b), None), span.into())
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
|
||||
let or_op = {
|
||||
let op = just(Token::Or).to(BinaryOps::Or);
|
||||
and_op
|
||||
.clone()
|
||||
.foldl(op.then(and_op).repeated(), |a, (op, b)| {
|
||||
let span = a.1.start..b.1.end;
|
||||
(Expr::binary_op(op, (a, b), None), span.into())
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
|
||||
let xor_op = {
|
||||
let op = just(Token::Xor).to(BinaryOps::Xor);
|
||||
or_op
|
||||
.clone()
|
||||
.foldl(op.then(or_op).repeated(), |a, (op, b)| {
|
||||
let span = a.1.start..b.1.end;
|
||||
(Expr::binary_op(op, (a, b), None), span.into())
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
|
||||
let implies_equiv_op = {
|
||||
let op = just(Token::Implies)
|
||||
.to(BinaryOps::Implies)
|
||||
.or(just(Token::Equiv).to(BinaryOps::Equiv));
|
||||
xor_op
|
||||
.clone()
|
||||
.foldl(op.then(xor_op).repeated(), |a, (op, b)| {
|
||||
let span = a.1.start..b.1.end;
|
||||
(Expr::binary_op(op, (a, b), None), span.into())
|
||||
})
|
||||
.boxed()
|
||||
};
|
||||
|
||||
implies_equiv_op
|
||||
.map(|(expr, span)| (expr.make_typed(Type::Bool), span))
|
||||
.labelled("boolean expression")
|
||||
.as_context()
|
||||
})
|
||||
}
|
||||
482
argus/src/semantics/boolean.rs
Normal file
482
argus/src/semantics/boolean.rs
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
use std::ops::Bound;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::utils::lemire_minmax::MonoWedge;
|
||||
use super::Trace;
|
||||
use crate::core::expr::*;
|
||||
use crate::core::signals::{InterpolationMethod, SignalPartialOrd};
|
||||
use crate::semantics::QuantitativeSemantics;
|
||||
use crate::{ArgusError, ArgusResult, Signal};
|
||||
|
||||
/// Boolean semantics for Signal Temporal Logic expressionsd define by an [`Expr`].
|
||||
pub struct BooleanSemantics;
|
||||
|
||||
impl BooleanSemantics {
|
||||
/// Evaluates a [Boolean expression](BoolExpr) given a [`Trace`].
|
||||
pub fn eval<BoolI, NumI>(expr: &BoolExpr, trace: &impl Trace) -> ArgusResult<Signal<bool>>
|
||||
where
|
||||
BoolI: InterpolationMethod<bool>,
|
||||
NumI: InterpolationMethod<f64>,
|
||||
{
|
||||
let ret = match expr {
|
||||
BoolExpr::BoolLit(val) => Signal::constant(val.0),
|
||||
BoolExpr::BoolVar(BoolVar { name }) => trace
|
||||
.get::<bool>(name.as_str())
|
||||
.ok_or(ArgusError::SignalNotPresent)?
|
||||
.clone(),
|
||||
BoolExpr::Cmp(Cmp { op, lhs, rhs }) => {
|
||||
use crate::core::expr::Ordering::*;
|
||||
let lhs = QuantitativeSemantics::eval_num_expr::<f64, NumI>(lhs, trace)?;
|
||||
let rhs = QuantitativeSemantics::eval_num_expr::<f64, NumI>(rhs, trace)?;
|
||||
|
||||
match op {
|
||||
Eq => lhs.signal_eq::<NumI>(&rhs).unwrap(),
|
||||
NotEq => lhs.signal_ne::<NumI>(&rhs).unwrap(),
|
||||
Less { strict } if *strict => lhs.signal_lt::<NumI>(&rhs).unwrap(),
|
||||
Less { strict: _ } => lhs.signal_le::<NumI>(&rhs).unwrap(),
|
||||
Greater { strict } if *strict => lhs.signal_gt::<NumI>(&rhs).unwrap(),
|
||||
Greater { strict: _ } => lhs.signal_ge::<NumI>(&rhs).unwrap(),
|
||||
}
|
||||
}
|
||||
BoolExpr::Not(Not { arg }) => {
|
||||
let arg = Self::eval::<BoolI, NumI>(arg, trace)?;
|
||||
!&arg
|
||||
}
|
||||
BoolExpr::And(And { args }) => {
|
||||
assert!(args.len() >= 2);
|
||||
args.iter().map(|arg| Self::eval::<BoolI, NumI>(arg, trace)).try_fold(
|
||||
Signal::const_true(),
|
||||
|acc, item| {
|
||||
let item = item?;
|
||||
Ok(acc.and::<BoolI>(&item))
|
||||
},
|
||||
)?
|
||||
}
|
||||
BoolExpr::Or(Or { args }) => {
|
||||
assert!(args.len() >= 2);
|
||||
args.iter().map(|arg| Self::eval::<BoolI, NumI>(arg, trace)).try_fold(
|
||||
Signal::const_true(),
|
||||
|acc, item| {
|
||||
let item = item?;
|
||||
Ok(acc.or::<BoolI>(&item))
|
||||
},
|
||||
)?
|
||||
}
|
||||
BoolExpr::Next(Next { arg }) => {
|
||||
let arg = Self::eval::<BoolI, NumI>(arg, trace)?;
|
||||
compute_next(arg)?
|
||||
}
|
||||
BoolExpr::Oracle(Oracle { steps, arg }) => {
|
||||
let arg = Self::eval::<BoolI, NumI>(arg, trace)?;
|
||||
compute_oracle(arg, *steps)?
|
||||
}
|
||||
BoolExpr::Always(Always { arg, interval }) => {
|
||||
let arg = Self::eval::<BoolI, NumI>(arg, trace)?;
|
||||
compute_always::<BoolI>(arg, interval)?
|
||||
}
|
||||
BoolExpr::Eventually(Eventually { arg, interval }) => {
|
||||
let arg = Self::eval::<BoolI, NumI>(arg, trace)?;
|
||||
compute_eventually::<BoolI>(arg, interval)?
|
||||
}
|
||||
BoolExpr::Until(Until { lhs, rhs, interval }) => {
|
||||
let lhs = Self::eval::<BoolI, NumI>(lhs, trace)?;
|
||||
let rhs = Self::eval::<BoolI, NumI>(rhs, trace)?;
|
||||
compute_until::<BoolI>(lhs, rhs, interval)?
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_next(arg: Signal<bool>) -> ArgusResult<Signal<bool>> {
|
||||
compute_oracle(arg, 1)
|
||||
}
|
||||
|
||||
fn compute_oracle(arg: Signal<bool>, steps: usize) -> ArgusResult<Signal<bool>> {
|
||||
if steps == 0 {
|
||||
return Ok(Signal::Empty);
|
||||
}
|
||||
match arg {
|
||||
Signal::Empty => Ok(Signal::Empty),
|
||||
sig @ Signal::Constant { value: _ } => {
|
||||
// Just return the signal as is
|
||||
Ok(sig)
|
||||
}
|
||||
Signal::Sampled {
|
||||
mut values,
|
||||
mut time_points,
|
||||
} => {
|
||||
// TODO(anand): Verify this
|
||||
// Just shift the signal by `steps` timestamps
|
||||
assert_eq!(values.len(), time_points.len());
|
||||
if values.len() <= steps {
|
||||
return Ok(Signal::Empty);
|
||||
}
|
||||
let expected_len = values.len() - steps;
|
||||
let values = values.split_off(steps);
|
||||
let _ = time_points.split_off(steps);
|
||||
|
||||
assert_eq!(values.len(), expected_len);
|
||||
assert_eq!(values.len(), time_points.len());
|
||||
Ok(Signal::Sampled { values, time_points })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute always for a signal
|
||||
fn compute_always<I: InterpolationMethod<bool>>(
|
||||
signal: Signal<bool>,
|
||||
interval: &Interval,
|
||||
) -> ArgusResult<Signal<bool>> {
|
||||
if interval.is_empty() || interval.is_singleton() {
|
||||
return Err(ArgusError::InvalidInterval {
|
||||
reason: "interval is either empty or singleton",
|
||||
});
|
||||
}
|
||||
let ret = match signal {
|
||||
// if signal is empty or constant, return the signal itself.
|
||||
// This works because if a signal is True everythere, then it must
|
||||
// "always be true".
|
||||
sig @ (Signal::Empty | Signal::Constant { value: _ }) => sig,
|
||||
sig => {
|
||||
use Bound::*;
|
||||
if interval.is_singleton() {
|
||||
// for singleton intervals, return the signal itself.
|
||||
sig
|
||||
} else if interval.is_untimed() {
|
||||
compute_untimed_always(sig)?
|
||||
} else if let (Included(a), Included(b)) = interval.into() {
|
||||
compute_timed_always::<I>(sig, *a, Some(*b))?
|
||||
} else if let (Included(a), Unbounded) = interval.into() {
|
||||
compute_timed_always::<I>(sig, *a, None)?
|
||||
} else {
|
||||
unreachable!("interval should be created using Interval::new, and is_untimed checks this")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Compute timed always for the interval `[a, b]` (or, if `b` is `None`, `[a, ..]`.
|
||||
fn compute_timed_always<I: InterpolationMethod<bool>>(
|
||||
signal: Signal<bool>,
|
||||
a: Duration,
|
||||
b: Option<Duration>,
|
||||
) -> ArgusResult<Signal<bool>> {
|
||||
let z1 = !signal;
|
||||
let z2 = compute_timed_eventually::<I>(z1, a, b)?;
|
||||
Ok(!z2)
|
||||
}
|
||||
|
||||
/// Compute untimed always
|
||||
fn compute_untimed_always(signal: Signal<bool>) -> ArgusResult<Signal<bool>> {
|
||||
let Signal::Sampled {
|
||||
mut values,
|
||||
time_points,
|
||||
} = signal
|
||||
else {
|
||||
unreachable!("we shouldn't be passing non-sampled signals here")
|
||||
};
|
||||
// Compute the & in a expanding window fashion from the back
|
||||
for i in (0..(time_points.len() - 1)).rev() {
|
||||
values[i] = values[i + 1].min(values[i]);
|
||||
}
|
||||
Ok(Signal::Sampled { values, time_points })
|
||||
}
|
||||
|
||||
/// Compute eventually for a signal
|
||||
fn compute_eventually<I: InterpolationMethod<bool>>(
|
||||
signal: Signal<bool>,
|
||||
interval: &Interval,
|
||||
) -> ArgusResult<Signal<bool>> {
|
||||
if interval.is_empty() || interval.is_singleton() {
|
||||
return Err(ArgusError::InvalidInterval {
|
||||
reason: "interval is either empty or singleton",
|
||||
});
|
||||
}
|
||||
let ret = match signal {
|
||||
// if signal is empty or constant, return the signal itself.
|
||||
// This works because if a signal is True everythere, then it must
|
||||
// "eventually be true".
|
||||
sig @ (Signal::Empty | Signal::Constant { value: _ }) => sig,
|
||||
sig => {
|
||||
use Bound::*;
|
||||
if interval.is_singleton() {
|
||||
// for singleton intervals, return the signal itself.
|
||||
sig
|
||||
} else if interval.is_untimed() {
|
||||
compute_untimed_eventually(sig)?
|
||||
} else if let (Included(a), Included(b)) = interval.into() {
|
||||
compute_timed_eventually::<I>(sig, *a, Some(*b))?
|
||||
} else if let (Included(a), Unbounded) = interval.into() {
|
||||
compute_timed_eventually::<I>(sig, *a, None)?
|
||||
} else {
|
||||
unreachable!("interval should be created using Interval::new, and is_untimed checks this")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Compute timed eventually for the interval `[a, b]` (or, if `b` is `None`, `[a,..]`.
|
||||
fn compute_timed_eventually<I: InterpolationMethod<bool>>(
|
||||
signal: Signal<bool>,
|
||||
a: Duration,
|
||||
b: Option<Duration>,
|
||||
) -> ArgusResult<Signal<bool>> {
|
||||
match b {
|
||||
Some(b) => {
|
||||
// We want to compute the windowed max/or of the signal.
|
||||
// The window is dictated by the time duration though.
|
||||
let Signal::Sampled { values, time_points } = signal else {
|
||||
unreachable!("we shouldn't be passing non-sampled signals here")
|
||||
};
|
||||
assert!(b > a);
|
||||
assert!(!time_points.is_empty());
|
||||
let signal_duration = *time_points.last().unwrap() - *time_points.first().unwrap();
|
||||
let width = if signal_duration < (b - a) {
|
||||
signal_duration
|
||||
} else {
|
||||
b - a
|
||||
};
|
||||
let mut ret_vals = Vec::with_capacity(values.len());
|
||||
|
||||
// For boolean signals we dont need to worry about intersections with ZERO as much as
|
||||
// for quantitative signals, as linear interpolation is just a discrte switch.
|
||||
let mut wedge = MonoWedge::<bool>::max_wedge(width);
|
||||
for (i, value) in time_points.iter().zip(&values) {
|
||||
wedge.update((i, value));
|
||||
if i >= &(time_points[0] + width) {
|
||||
ret_vals.push(
|
||||
wedge
|
||||
.front()
|
||||
.map(|(&t, &v)| (t, v))
|
||||
.unwrap_or_else(|| panic!("wedge should have at least 1 element")),
|
||||
)
|
||||
}
|
||||
}
|
||||
Signal::try_from_iter(ret_vals)
|
||||
}
|
||||
None => {
|
||||
// Shift the signal to the left by `a` and then run the untimed eventually.
|
||||
let shifted = signal.shift_left::<I>(a);
|
||||
compute_untimed_eventually(shifted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute untimed eventually
|
||||
fn compute_untimed_eventually(signal: Signal<bool>) -> ArgusResult<Signal<bool>> {
|
||||
let Signal::Sampled {
|
||||
mut values,
|
||||
time_points,
|
||||
} = signal
|
||||
else {
|
||||
unreachable!("we shouldn't be passing non-sampled signals here")
|
||||
};
|
||||
// Compute the | in a expanding window fashion from the back
|
||||
for i in (0..(time_points.len() - 1)).rev() {
|
||||
values[i] = values[i + 1].max(values[i]);
|
||||
}
|
||||
Ok(Signal::Sampled { values, time_points })
|
||||
}
|
||||
|
||||
/// Compute until
|
||||
fn compute_until<I: InterpolationMethod<bool>>(
|
||||
lhs: Signal<bool>,
|
||||
rhs: Signal<bool>,
|
||||
interval: &Interval,
|
||||
) -> ArgusResult<Signal<bool>> {
|
||||
let ret = match (lhs, rhs) {
|
||||
// If either signals are empty, return empty
|
||||
(sig @ Signal::Empty, _) | (_, sig @ Signal::Empty) => sig,
|
||||
(lhs, rhs) => {
|
||||
use Bound::*;
|
||||
if interval.is_untimed() {
|
||||
compute_untimed_until::<I>(lhs, rhs)?
|
||||
} else if let (Included(a), Included(b)) = interval.into() {
|
||||
compute_timed_until::<I>(lhs, rhs, *a, Some(*b))?
|
||||
} else if let (Included(a), Unbounded) = interval.into() {
|
||||
compute_timed_until::<I>(lhs, rhs, *a, None)?
|
||||
} else {
|
||||
unreachable!("interval should be created using Interval::new, and is_untimed checks this")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Compute timed until for the interval `[a, b]` (or, if `b` is `None`, `[a, ..]`.
|
||||
///
|
||||
/// For this, we will perform the Until rewrite defined in [1]:
|
||||
/// $$
|
||||
/// \varphi_1 U_{[a, b]} \varphi_2 = F_{[a,b]} \varphi_2 \land (\varphi_1 U_{[a,
|
||||
/// \infty)} \varphi_2)
|
||||
/// $$
|
||||
///
|
||||
/// $$
|
||||
/// \varphi_1 U_{[a, \infty)} \varphi_2 = G_{[0,a]} (\varphi_1 U \varphi_2)
|
||||
/// $$
|
||||
///
|
||||
/// [1]: <> (A. Donzé, T. Ferrère, and O. Maler, "Efficient Robust Monitoring for STL.")
|
||||
fn compute_timed_until<I: InterpolationMethod<bool>>(
|
||||
lhs: Signal<bool>,
|
||||
rhs: Signal<bool>,
|
||||
a: Duration,
|
||||
b: Option<Duration>,
|
||||
) -> ArgusResult<Signal<bool>> {
|
||||
match b {
|
||||
Some(b) => {
|
||||
// First compute eventually [a, b]
|
||||
let ev_a_b_rhs = compute_timed_eventually::<I>(rhs.clone(), a, Some(b))?;
|
||||
// Then compute until [a, \infty) (lhs, rhs)
|
||||
let unt_a_inf = compute_timed_until::<I>(lhs, rhs, a, None)?;
|
||||
// Then & them
|
||||
Ok(ev_a_b_rhs.and::<I>(&unt_a_inf))
|
||||
}
|
||||
None => {
|
||||
assert_ne!(a, Duration::ZERO, "untimed case wasn't handled for Until");
|
||||
// First compute untimed until (lhs, rhs)
|
||||
let untimed_until = compute_untimed_until::<I>(lhs, rhs)?;
|
||||
// Compute G [0, a]
|
||||
compute_timed_always::<I>(untimed_until, Duration::ZERO, Some(a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute untimed until
|
||||
fn compute_untimed_until<I: InterpolationMethod<bool>>(
|
||||
lhs: Signal<bool>,
|
||||
rhs: Signal<bool>,
|
||||
) -> ArgusResult<Signal<bool>> {
|
||||
let sync_points = lhs.sync_with_intersection::<I>(&rhs).unwrap();
|
||||
let mut ret_samples = Vec::with_capacity(sync_points.len());
|
||||
let expected_len = sync_points.len();
|
||||
|
||||
let mut next = false;
|
||||
|
||||
for (i, t) in sync_points.into_iter().enumerate().rev() {
|
||||
let v1 = lhs.interpolate_at::<I>(t).unwrap();
|
||||
let v2 = rhs.interpolate_at::<I>(t).unwrap();
|
||||
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
let z = (v1 && v2) || (v1 && next);
|
||||
if z == next && i < (expected_len - 2) {
|
||||
ret_samples.pop();
|
||||
}
|
||||
ret_samples.push((t, z));
|
||||
next = z;
|
||||
}
|
||||
|
||||
Signal::<bool>::try_from_iter(ret_samples.into_iter().rev())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use itertools::assert_equal;
|
||||
|
||||
use super::*;
|
||||
use crate::core::expr::ExprBuilder;
|
||||
use crate::core::signals::interpolation::Linear;
|
||||
use crate::core::signals::AnySignal;
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyTrace {
|
||||
signals: HashMap<String, Box<dyn AnySignal>>,
|
||||
}
|
||||
|
||||
impl Trace for MyTrace {
|
||||
fn signal_names(&self) -> Vec<&str> {
|
||||
self.signals.keys().map(|key| key.as_str()).collect()
|
||||
}
|
||||
|
||||
fn get<T: 'static>(&self, name: &str) -> Option<&Signal<T>> {
|
||||
let signal = self.signals.get(name)?;
|
||||
signal.as_any().downcast_ref::<Signal<T>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn less_than() {
|
||||
let mut ctx = ExprBuilder::new();
|
||||
|
||||
let a = Box::new(ctx.float_var("a".to_owned()).unwrap());
|
||||
let spec = ctx.make_lt(a, Box::new(ctx.float_const(0.0)));
|
||||
|
||||
let signals = HashMap::from_iter(vec![(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 1.3),
|
||||
(Duration::from_secs_f64(0.7), 3.0),
|
||||
(Duration::from_secs_f64(1.3), 0.1),
|
||||
(Duration::from_secs_f64(2.1), -2.2),
|
||||
])) as Box<dyn AnySignal>,
|
||||
)]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
|
||||
let rob = BooleanSemantics::eval::<Linear, Linear>(&spec, &trace).unwrap();
|
||||
let expected = Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), false),
|
||||
(Duration::from_secs_f64(0.7), false),
|
||||
(Duration::from_secs_f64(1.3), false),
|
||||
(Duration::from_secs_f64(1.334782609), true), // interpolated at
|
||||
(Duration::from_secs_f64(2.1), true),
|
||||
]);
|
||||
|
||||
assert_equal(&rob, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eventually_unbounded() {
|
||||
let mut ctx = ExprBuilder::new();
|
||||
|
||||
let a = Box::new(ctx.float_var("a".to_owned()).unwrap());
|
||||
let cmp = Box::new(ctx.make_ge(a, Box::new(ctx.float_const(0.0))));
|
||||
let spec = ctx.make_eventually(cmp);
|
||||
|
||||
{
|
||||
let signals = HashMap::from_iter(vec![(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 2.5),
|
||||
(Duration::from_secs_f64(0.7), 4.0),
|
||||
(Duration::from_secs_f64(1.3), -1.0),
|
||||
(Duration::from_secs_f64(2.1), 1.7),
|
||||
])) as Box<dyn AnySignal>,
|
||||
)]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
let rob = BooleanSemantics::eval::<Linear, Linear>(&spec, &trace).unwrap();
|
||||
|
||||
let Signal::Sampled { values, time_points: _ } = rob else {
|
||||
panic!("boolean semantics should remain sampled");
|
||||
};
|
||||
assert!(values.into_iter().all(|v| v));
|
||||
}
|
||||
{
|
||||
let signals = HashMap::from_iter(vec![(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 2.5),
|
||||
(Duration::from_secs_f64(0.7), 4.0),
|
||||
(Duration::from_secs_f64(1.3), 1.7),
|
||||
(Duration::from_secs_f64(1.4), 0.0),
|
||||
(Duration::from_secs_f64(2.1), -2.0),
|
||||
])) as Box<dyn AnySignal>,
|
||||
)]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
let rob = BooleanSemantics::eval::<Linear, Linear>(&spec, &trace).unwrap();
|
||||
println!("{:#?}", rob);
|
||||
|
||||
let Signal::Sampled { values, time_points: _ } = rob else {
|
||||
panic!("boolean semantics should remain sampled");
|
||||
};
|
||||
assert!(values[..values.len() - 1].iter().all(|&v| v));
|
||||
assert!(!values[values.len() - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
argus/src/semantics/mod.rs
Normal file
61
argus/src/semantics/mod.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
//! Argus offline semantics
|
||||
//!
|
||||
//! In this crate, we are predominantly concerned with the monitoring of _offline system
|
||||
//! traces_, i.e., a collection of signals that have been extracted from observing and
|
||||
//! sampling from some system.
|
||||
|
||||
mod boolean;
|
||||
mod quantitative;
|
||||
pub mod utils;
|
||||
|
||||
pub use boolean::BooleanSemantics;
|
||||
pub use quantitative::QuantitativeSemantics;
|
||||
|
||||
use crate::Signal;
|
||||
|
||||
/// A trace is a collection of signals
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// An example of a `Trace` may be:
|
||||
///
|
||||
/// ```rust
|
||||
/// use argus::{Signal, AnySignal, Trace};
|
||||
///
|
||||
/// struct MyTrace {
|
||||
/// x: Signal<bool>,
|
||||
/// y: Signal<i64>,
|
||||
/// }
|
||||
///
|
||||
/// impl Trace for MyTrace {
|
||||
/// fn signal_names(&self) -> Vec<&str> {
|
||||
/// vec!["x", "y"]
|
||||
/// }
|
||||
///
|
||||
/// fn get<T: 'static>(&self, name: &str) -> Option<&Signal<T>> {
|
||||
/// let sig: &dyn AnySignal = match name {
|
||||
/// "x" => &self.x,
|
||||
/// "y" => &self.y,
|
||||
/// _ => return None,
|
||||
/// };
|
||||
/// sig.as_any().downcast_ref::<Signal<T>>()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let trace = MyTrace {
|
||||
/// x: Signal::constant(true),
|
||||
/// y: Signal::constant(2),
|
||||
/// };
|
||||
/// let names = trace.signal_names();
|
||||
///
|
||||
/// assert!(names == &["x", "y"] || names == &["y", "x"]);
|
||||
/// assert!(matches!(trace.get::<bool>("x"), Some(Signal::Constant { value: true })));
|
||||
/// assert!(matches!(trace.get::<i64>("y"), Some(Signal::Constant { value: 2 })));
|
||||
/// ```
|
||||
pub trait Trace {
|
||||
/// Get the list of signal names contained within the trace.
|
||||
fn signal_names(&self) -> Vec<&str>;
|
||||
|
||||
/// Query a signal using its name
|
||||
fn get<T: 'static>(&self, name: &str) -> Option<&Signal<T>>;
|
||||
}
|
||||
707
argus/src/semantics/quantitative.rs
Normal file
707
argus/src/semantics/quantitative.rs
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
use std::ops::Bound;
|
||||
use std::time::Duration;
|
||||
|
||||
use num_traits::{Num, NumCast};
|
||||
|
||||
use super::utils::lemire_minmax::MonoWedge;
|
||||
use super::Trace;
|
||||
use crate::core::expr::*;
|
||||
use crate::core::signals::{InterpolationMethod, SignalAbs};
|
||||
use crate::{ArgusError, ArgusResult, Signal};
|
||||
|
||||
/// Quantitative semantics for Signal Temporal Logic expressionsd define by an [`Expr`].
|
||||
pub struct QuantitativeSemantics;
|
||||
|
||||
impl QuantitativeSemantics {
|
||||
/// Evaluates a [Boolean expression](BoolExpr) given a [`Trace`].
|
||||
pub fn eval<I>(expr: &BoolExpr, trace: &impl Trace) -> ArgusResult<Signal<f64>>
|
||||
where
|
||||
I: InterpolationMethod<f64>,
|
||||
{
|
||||
let ret = match expr {
|
||||
BoolExpr::BoolLit(val) => top_or_bot(&Signal::constant(val.0)),
|
||||
BoolExpr::BoolVar(BoolVar { name }) => trace
|
||||
.get::<bool>(name.as_str())
|
||||
.ok_or(ArgusError::SignalNotPresent)
|
||||
.map(top_or_bot)?,
|
||||
BoolExpr::Cmp(Cmp { op, lhs, rhs }) => {
|
||||
use crate::core::expr::Ordering::*;
|
||||
let lhs = Self::eval_num_expr::<f64, I>(lhs, trace)?;
|
||||
let rhs = Self::eval_num_expr::<f64, I>(rhs, trace)?;
|
||||
|
||||
match op {
|
||||
Eq => lhs.abs_diff::<_, I>(&rhs).negate(),
|
||||
NotEq => lhs.abs_diff::<_, I>(&rhs).negate(),
|
||||
Less { strict: _ } => rhs.sub::<_, I>(&lhs),
|
||||
Greater { strict: _ } => lhs.sub::<_, I>(&rhs),
|
||||
}
|
||||
}
|
||||
BoolExpr::Not(Not { arg }) => Self::eval::<I>(arg, trace)?.negate(),
|
||||
BoolExpr::And(And { args }) => {
|
||||
assert!(args.len() >= 2);
|
||||
args.iter().map(|arg| Self::eval::<I>(arg, trace)).try_fold(
|
||||
Signal::constant(f64::INFINITY),
|
||||
|acc, item| {
|
||||
let item = item?;
|
||||
Ok(acc.min::<I>(&item))
|
||||
},
|
||||
)?
|
||||
}
|
||||
BoolExpr::Or(Or { args }) => {
|
||||
assert!(args.len() >= 2);
|
||||
args.iter().map(|arg| Self::eval::<I>(arg, trace)).try_fold(
|
||||
Signal::constant(f64::NEG_INFINITY),
|
||||
|acc, item| {
|
||||
let item = item?;
|
||||
Ok(acc.max::<I>(&item))
|
||||
},
|
||||
)?
|
||||
}
|
||||
BoolExpr::Next(Next { arg }) => {
|
||||
let arg = Self::eval::<I>(arg, trace)?;
|
||||
compute_next(arg)?
|
||||
}
|
||||
BoolExpr::Oracle(Oracle { steps, arg }) => {
|
||||
let arg = Self::eval::<I>(arg, trace)?;
|
||||
compute_oracle(arg, *steps)?
|
||||
}
|
||||
BoolExpr::Always(Always { arg, interval }) => {
|
||||
let arg = Self::eval::<I>(arg, trace)?;
|
||||
compute_always::<I>(arg, interval)?
|
||||
}
|
||||
BoolExpr::Eventually(Eventually { arg, interval }) => {
|
||||
let arg = Self::eval::<I>(arg, trace)?;
|
||||
compute_eventually::<I>(arg, interval)?
|
||||
}
|
||||
BoolExpr::Until(Until { lhs, rhs, interval }) => {
|
||||
let lhs = Self::eval::<I>(lhs, trace)?;
|
||||
let rhs = Self::eval::<I>(rhs, trace)?;
|
||||
compute_until::<I>(lhs, rhs, interval)?
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Evaluates a [numeric expression](NumExpr) given a [`Trace`].
|
||||
pub fn eval_num_expr<T, I>(root: &NumExpr, trace: &impl Trace) -> ArgusResult<Signal<T>>
|
||||
where
|
||||
T: Num + NumCast + Clone + PartialOrd,
|
||||
for<'a> &'a T: core::ops::Neg<Output = T>,
|
||||
for<'a> &'a T: core::ops::Add<&'a T, Output = T>,
|
||||
for<'a> &'a T: core::ops::Sub<&'a T, Output = T>,
|
||||
for<'a> &'a T: core::ops::Mul<&'a T, Output = T>,
|
||||
for<'a> &'a T: core::ops::Div<&'a T, Output = T>,
|
||||
Signal<T>: SignalAbs,
|
||||
I: InterpolationMethod<T>,
|
||||
{
|
||||
match root {
|
||||
NumExpr::IntLit(val) => Signal::constant(val.0).num_cast(),
|
||||
NumExpr::UIntLit(val) => Signal::constant(val.0).num_cast(),
|
||||
NumExpr::FloatLit(val) => Signal::constant(val.0).num_cast(),
|
||||
NumExpr::IntVar(IntVar { name }) => trace.get::<i64>(name.as_str()).unwrap().num_cast(),
|
||||
NumExpr::UIntVar(UIntVar { name }) => trace.get::<u64>(name.as_str()).unwrap().num_cast(),
|
||||
NumExpr::FloatVar(FloatVar { name }) => trace.get::<f64>(name.as_str()).unwrap().num_cast(),
|
||||
NumExpr::Neg(Neg { arg }) => Self::eval_num_expr::<T, I>(arg, trace).map(|sig| sig.negate()),
|
||||
NumExpr::Add(Add { args }) => {
|
||||
let mut ret: Signal<T> = Signal::<T>::zero();
|
||||
for arg in args.iter() {
|
||||
let arg = Self::eval_num_expr::<T, I>(arg, trace)?;
|
||||
ret = ret.add::<_, I>(&arg);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
NumExpr::Sub(Sub { lhs, rhs }) => {
|
||||
let lhs = Self::eval_num_expr::<T, I>(lhs, trace)?;
|
||||
let rhs = Self::eval_num_expr::<T, I>(rhs, trace)?;
|
||||
Ok(lhs.sub::<_, I>(&rhs))
|
||||
}
|
||||
NumExpr::Mul(Mul { args }) => {
|
||||
let mut ret: Signal<T> = Signal::<T>::one();
|
||||
for arg in args.iter() {
|
||||
let arg = Self::eval_num_expr::<T, I>(arg, trace)?;
|
||||
ret = ret.mul::<_, I>(&arg);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
NumExpr::Div(Div { dividend, divisor }) => {
|
||||
let dividend = Self::eval_num_expr::<T, I>(dividend, trace)?;
|
||||
let divisor = Self::eval_num_expr::<T, I>(divisor, trace)?;
|
||||
Ok(dividend.div::<_, I>(&divisor))
|
||||
}
|
||||
NumExpr::Abs(Abs { arg }) => {
|
||||
let arg = Self::eval_num_expr::<T, I>(arg, trace)?;
|
||||
Ok(arg.abs())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_next(arg: Signal<f64>) -> ArgusResult<Signal<f64>> {
|
||||
compute_oracle(arg, 1)
|
||||
}
|
||||
|
||||
fn compute_oracle(arg: Signal<f64>, steps: usize) -> ArgusResult<Signal<f64>> {
|
||||
if steps == 0 {
|
||||
return Ok(Signal::Empty);
|
||||
}
|
||||
match arg {
|
||||
Signal::Empty => Ok(Signal::Empty),
|
||||
sig @ Signal::Constant { value: _ } => {
|
||||
// Just return the signal as is
|
||||
Ok(sig)
|
||||
}
|
||||
Signal::Sampled {
|
||||
mut values,
|
||||
mut time_points,
|
||||
} => {
|
||||
// TODO(anand): Verify this
|
||||
// Just shift the signal by `steps` timestamps
|
||||
assert_eq!(values.len(), time_points.len());
|
||||
if values.len() <= steps {
|
||||
return Ok(Signal::Empty);
|
||||
}
|
||||
let expected_len = values.len() - steps;
|
||||
let values = values.split_off(steps);
|
||||
let _ = time_points.split_off(steps);
|
||||
|
||||
assert_eq!(values.len(), expected_len);
|
||||
assert_eq!(values.len(), time_points.len());
|
||||
Ok(Signal::Sampled { values, time_points })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute always for a signal
|
||||
fn compute_always<I: InterpolationMethod<f64>>(signal: Signal<f64>, interval: &Interval) -> ArgusResult<Signal<f64>> {
|
||||
if interval.is_empty() || interval.is_singleton() {
|
||||
return Err(ArgusError::InvalidInterval {
|
||||
reason: "interval is either empty or singleton",
|
||||
});
|
||||
}
|
||||
let ret = match signal {
|
||||
// if signal is empty or constant, return the signal itself.
|
||||
// This works because if a signal is True everythere, then it must
|
||||
// "always be true".
|
||||
sig @ (Signal::Empty | Signal::Constant { value: _ }) => sig,
|
||||
sig => {
|
||||
use Bound::*;
|
||||
if interval.is_singleton() {
|
||||
// for singleton intervals, return the signal itself.
|
||||
sig
|
||||
} else if interval.is_untimed() {
|
||||
compute_untimed_always::<I>(sig)?
|
||||
} else if let (Included(a), Included(b)) = interval.into() {
|
||||
compute_timed_always::<I>(sig, *a, Some(*b))?
|
||||
} else if let (Included(a), Unbounded) = interval.into() {
|
||||
compute_timed_always::<I>(sig, *a, None)?
|
||||
} else {
|
||||
unreachable!("interval should be created using Interval::new, and is_untimed checks this")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Compute timed always for the interval `[a, b]` (or, if `b` is `None`, `[a, ..]`.
|
||||
fn compute_timed_always<I: InterpolationMethod<f64>>(
|
||||
signal: Signal<f64>,
|
||||
a: Duration,
|
||||
b: Option<Duration>,
|
||||
) -> ArgusResult<Signal<f64>> {
|
||||
let z1 = signal.negate();
|
||||
let z2 = compute_timed_eventually::<I>(z1, a, b)?;
|
||||
Ok(z2.negate())
|
||||
}
|
||||
|
||||
/// Compute untimed always
|
||||
fn compute_untimed_always<I: InterpolationMethod<f64>>(signal: Signal<f64>) -> ArgusResult<Signal<f64>> {
|
||||
// Find all the points where the argument signal crosses 0
|
||||
let Some(time_points) = signal.sync_with_intersection::<I>(&Signal::constant(0.0)) else {
|
||||
unreachable!("we shouldn't be passing non-sampled signals here")
|
||||
};
|
||||
let mut values: Vec<f64> = time_points
|
||||
.iter()
|
||||
.map(|&t| signal.interpolate_at::<I>(t).unwrap())
|
||||
.collect();
|
||||
// Compute the & in a expanding window fashion from the back
|
||||
for i in (0..(time_points.len() - 1)).rev() {
|
||||
values[i] = values[i + 1].min(values[i]);
|
||||
}
|
||||
Ok(Signal::Sampled { values, time_points })
|
||||
}
|
||||
|
||||
/// Compute eventually for a signal
|
||||
fn compute_eventually<I: InterpolationMethod<f64>>(
|
||||
signal: Signal<f64>,
|
||||
interval: &Interval,
|
||||
) -> ArgusResult<Signal<f64>> {
|
||||
if interval.is_empty() || interval.is_singleton() {
|
||||
return Err(ArgusError::InvalidInterval {
|
||||
reason: "interval is either empty or singleton",
|
||||
});
|
||||
}
|
||||
let ret = match signal {
|
||||
// if signal is empty or constant, return the signal itself.
|
||||
// This works because if a signal is True everythere, then it must
|
||||
// "eventually be true".
|
||||
sig @ (Signal::Empty | Signal::Constant { value: _ }) => sig,
|
||||
sig => {
|
||||
use Bound::*;
|
||||
if interval.is_singleton() {
|
||||
// for singleton intervals, return the signal itself.
|
||||
sig
|
||||
} else if interval.is_untimed() {
|
||||
compute_untimed_eventually::<I>(sig)?
|
||||
} else if let (Included(a), Included(b)) = interval.into() {
|
||||
compute_timed_eventually::<I>(sig, *a, Some(*b))?
|
||||
} else if let (Included(a), Unbounded) = interval.into() {
|
||||
compute_timed_eventually::<I>(sig, *a, None)?
|
||||
} else {
|
||||
unreachable!("interval should be created using Interval::new, and is_untimed checks this")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Compute timed eventually for the interval `[a, b]` (or, if `b` is `None`, `[a,..]`.
|
||||
fn compute_timed_eventually<I: InterpolationMethod<f64>>(
|
||||
signal: Signal<f64>,
|
||||
a: Duration,
|
||||
b: Option<Duration>,
|
||||
) -> ArgusResult<Signal<f64>> {
|
||||
match b {
|
||||
Some(b) => {
|
||||
// We want to compute the windowed max/or of the signal.
|
||||
// The window is dictated by the time duration though.
|
||||
let Signal::Sampled { values, time_points } = signal else {
|
||||
unreachable!("we shouldn't be passing non-sampled signals here")
|
||||
};
|
||||
assert!(b > a);
|
||||
assert!(!time_points.is_empty());
|
||||
let signal_duration = *time_points.last().unwrap() - *time_points.first().unwrap();
|
||||
let width = if signal_duration < (b - a) {
|
||||
signal_duration
|
||||
} else {
|
||||
b - a
|
||||
};
|
||||
let mut ret_vals = Vec::with_capacity(values.len());
|
||||
|
||||
// For boolean signals we dont need to worry about intersections with ZERO as much as
|
||||
// for quantitative signals, as linear interpolation is just a discrte switch.
|
||||
let mut wedge = MonoWedge::<f64>::max_wedge(width);
|
||||
for (i, value) in time_points.iter().zip(&values) {
|
||||
wedge.update((i, value));
|
||||
if i >= &(time_points[0] + width) {
|
||||
ret_vals.push(
|
||||
wedge
|
||||
.front()
|
||||
.map(|(&t, &v)| (t, v))
|
||||
.unwrap_or_else(|| panic!("wedge should have at least 1 element")),
|
||||
)
|
||||
}
|
||||
}
|
||||
Signal::try_from_iter(ret_vals)
|
||||
}
|
||||
None => {
|
||||
// Shift the signal to the left by `a` and then run the untimed eventually.
|
||||
let shifted = signal.shift_left::<I>(a);
|
||||
compute_untimed_eventually::<I>(shifted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute untimed eventually
|
||||
fn compute_untimed_eventually<I: InterpolationMethod<f64>>(signal: Signal<f64>) -> ArgusResult<Signal<f64>> {
|
||||
// Find all the points where the argument signal crosses 0
|
||||
let Some(time_points) = signal.sync_with_intersection::<I>(&Signal::constant(0.0)) else {
|
||||
unreachable!("we shouldn't be passing non-sampled signals here")
|
||||
};
|
||||
let mut values: Vec<f64> = time_points
|
||||
.iter()
|
||||
.map(|&t| signal.interpolate_at::<I>(t).unwrap())
|
||||
.collect();
|
||||
// Compute the | in a expanding window fashion from the back
|
||||
for i in (0..(time_points.len() - 1)).rev() {
|
||||
values[i] = values[i + 1].max(values[i]);
|
||||
}
|
||||
Ok(Signal::Sampled { values, time_points })
|
||||
}
|
||||
|
||||
/// Compute until
|
||||
fn compute_until<I: InterpolationMethod<f64>>(
|
||||
lhs: Signal<f64>,
|
||||
rhs: Signal<f64>,
|
||||
interval: &Interval,
|
||||
) -> ArgusResult<Signal<f64>> {
|
||||
let ret = match (lhs, rhs) {
|
||||
// If either signals are empty, return empty
|
||||
(sig @ Signal::Empty, _) | (_, sig @ Signal::Empty) => sig,
|
||||
(lhs, rhs) => {
|
||||
use Bound::*;
|
||||
if interval.is_untimed() {
|
||||
compute_untimed_until::<I>(lhs, rhs)?
|
||||
} else if let (Included(a), Included(b)) = interval.into() {
|
||||
compute_timed_until::<I>(lhs, rhs, *a, Some(*b))?
|
||||
} else if let (Included(a), Unbounded) = interval.into() {
|
||||
compute_timed_until::<I>(lhs, rhs, *a, None)?
|
||||
} else {
|
||||
unreachable!("interval should be created using Interval::new, and is_untimed checks this")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Compute timed until for the interval `[a, b]` (or, if `b` is `None`, `[a, ..]`.
|
||||
///
|
||||
/// For this, we will perform the Until rewrite defined in [1]:
|
||||
/// $$
|
||||
/// \varphi_1 U_{[a, b]} \varphi_2 = F_{[a,b]} \varphi_2 \land (\varphi_1 U_{[a,
|
||||
/// \infty)} \varphi_2)
|
||||
/// $$
|
||||
///
|
||||
/// $$
|
||||
/// \varphi_1 U_{[a, \infty)} \varphi_2 = G_{[0,a]} (\varphi_1 U \varphi_2)
|
||||
/// $$
|
||||
///
|
||||
/// [1]: <> (A. Donzé, T. Ferrère, and O. Maler, "Efficient Robust Monitoring for STL.")
|
||||
fn compute_timed_until<I: InterpolationMethod<f64>>(
|
||||
lhs: Signal<f64>,
|
||||
rhs: Signal<f64>,
|
||||
a: Duration,
|
||||
b: Option<Duration>,
|
||||
) -> ArgusResult<Signal<f64>> {
|
||||
match b {
|
||||
Some(b) => {
|
||||
// First compute eventually [a, b]
|
||||
let ev_a_b_rhs = compute_timed_eventually::<I>(rhs.clone(), a, Some(b))?;
|
||||
// Then compute until [a, \infty) (lhs, rhs)
|
||||
let unt_a_inf = compute_timed_until::<I>(lhs, rhs, a, None)?;
|
||||
// Then & them
|
||||
Ok(ev_a_b_rhs.min::<I>(&unt_a_inf))
|
||||
}
|
||||
None => {
|
||||
assert_ne!(a, Duration::ZERO, "untimed case wasn't handled for Until");
|
||||
// First compute untimed until (lhs, rhs)
|
||||
let untimed_until = compute_untimed_until::<I>(lhs, rhs)?;
|
||||
// Compute G [0, a]
|
||||
compute_timed_always::<I>(untimed_until, Duration::ZERO, Some(a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute untimed until
|
||||
fn compute_untimed_until<I: InterpolationMethod<f64>>(lhs: Signal<f64>, rhs: Signal<f64>) -> ArgusResult<Signal<f64>> {
|
||||
let sync_points = lhs.sync_with_intersection::<I>(&rhs).unwrap();
|
||||
let mut ret_samples = Vec::with_capacity(sync_points.len());
|
||||
let expected_len = sync_points.len();
|
||||
|
||||
let mut next = f64::NEG_INFINITY;
|
||||
|
||||
for (i, t) in sync_points.into_iter().enumerate().rev() {
|
||||
let v1 = lhs.interpolate_at::<I>(t).unwrap();
|
||||
let v2 = rhs.interpolate_at::<I>(t).unwrap();
|
||||
|
||||
let z = f64::max(f64::min(v1, v2), f64::min(v1, next));
|
||||
if z == next && i < (expected_len - 2) {
|
||||
ret_samples.pop();
|
||||
}
|
||||
ret_samples.push((t, z));
|
||||
next = z;
|
||||
}
|
||||
|
||||
Signal::<f64>::try_from_iter(ret_samples.into_iter().rev())
|
||||
}
|
||||
|
||||
fn top_or_bot(sig: &Signal<bool>) -> Signal<f64> {
|
||||
let bool2float = |&v| {
|
||||
if v {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
}
|
||||
};
|
||||
match sig {
|
||||
Signal::Empty => Signal::Empty,
|
||||
Signal::Constant { value } => Signal::constant(bool2float(value)),
|
||||
Signal::Sampled { values, time_points } => {
|
||||
time_points.iter().copied().zip(values.iter().map(bool2float)).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::iter::zip;
|
||||
use std::time::Duration;
|
||||
|
||||
use itertools::assert_equal;
|
||||
|
||||
use super::*;
|
||||
use crate::core::expr::ExprBuilder;
|
||||
use crate::core::signals::interpolation::{Constant, Linear};
|
||||
use crate::core::signals::AnySignal;
|
||||
|
||||
const FLOAT_EPS: f64 = 1.0e-8;
|
||||
|
||||
fn assert_approx_eq(lhs: &Signal<f64>, rhs: &Signal<f64>) {
|
||||
zip(lhs, rhs).enumerate().for_each(|(i, (s1, s2))| {
|
||||
assert_eq!(
|
||||
s1.0, s2.0,
|
||||
"Failed assertion {:?} != {:?} for iteration {}",
|
||||
s1.0, s2.0, i
|
||||
);
|
||||
assert!(
|
||||
(s2.1 - s1.1).abs() <= FLOAT_EPS,
|
||||
"Failed approx equal assertion: {} != {} for iteration {}",
|
||||
s1.1,
|
||||
s2.1,
|
||||
i
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyTrace {
|
||||
signals: HashMap<String, Box<dyn AnySignal>>,
|
||||
}
|
||||
|
||||
impl Trace for MyTrace {
|
||||
fn signal_names(&self) -> Vec<&str> {
|
||||
self.signals.keys().map(|key| key.as_str()).collect()
|
||||
}
|
||||
|
||||
fn get<T: 'static>(&self, name: &str) -> Option<&Signal<T>> {
|
||||
let signal = self.signals.get(name)?;
|
||||
signal.as_any().downcast_ref::<Signal<T>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_constant() {
|
||||
let expr_builder = ExprBuilder::new();
|
||||
|
||||
let spec = expr_builder.float_const(5.0);
|
||||
let trace = MyTrace::default();
|
||||
|
||||
let robustness = QuantitativeSemantics::eval_num_expr::<f64, Linear>(&spec, &trace).unwrap();
|
||||
|
||||
assert!(matches!(robustness, Signal::Constant { value } if value == 5.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addition() {
|
||||
let mut ctx = ExprBuilder::new();
|
||||
|
||||
let a = Box::new(ctx.float_var("a".to_owned()).unwrap());
|
||||
let b = Box::new(ctx.float_var("b".to_owned()).unwrap());
|
||||
let spec = ctx.make_add([*a, *b]).unwrap();
|
||||
|
||||
{
|
||||
let signals = HashMap::from_iter(vec![
|
||||
(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 1.3),
|
||||
(Duration::from_secs_f64(0.7), 3.0),
|
||||
(Duration::from_secs_f64(1.3), 0.1),
|
||||
(Duration::from_secs_f64(2.1), -2.2),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
(
|
||||
"b".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 2.5),
|
||||
(Duration::from_secs_f64(0.7), 4.0),
|
||||
(Duration::from_secs_f64(1.3), -1.2),
|
||||
(Duration::from_secs_f64(2.1), 1.7),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
|
||||
let rob = QuantitativeSemantics::eval_num_expr::<f64, Linear>(&spec, &trace).unwrap();
|
||||
let expected = Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 1.3 + 2.5),
|
||||
(Duration::from_secs_f64(0.7), 3.0 + 4.0),
|
||||
(Duration::from_secs_f64(1.3), 0.1 + -1.2),
|
||||
(Duration::from_secs_f64(2.1), -2.2 + 1.7),
|
||||
]);
|
||||
|
||||
assert_equal(&rob, &expected);
|
||||
}
|
||||
{
|
||||
let signals = HashMap::from_iter(vec![
|
||||
(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 1.3),
|
||||
(Duration::from_secs_f64(0.7), 3.0),
|
||||
(Duration::from_secs_f64(1.3), 4.0),
|
||||
(Duration::from_secs_f64(2.1), 3.0),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
(
|
||||
"b".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 2.5),
|
||||
(Duration::from_secs_f64(0.7), 4.0),
|
||||
(Duration::from_secs_f64(1.3), 3.0),
|
||||
(Duration::from_secs_f64(2.1), 4.0),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
|
||||
let rob = QuantitativeSemantics::eval_num_expr::<f64, Linear>(&spec, &trace).unwrap();
|
||||
let expected = Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 1.3 + 2.5),
|
||||
(Duration::from_secs_f64(0.7), 3.0 + 4.0),
|
||||
(Duration::from_secs_f64(1.3), 4.0 + 3.0),
|
||||
(Duration::from_secs_f64(2.1), 3.0 + 4.0),
|
||||
]);
|
||||
|
||||
assert_equal(&rob, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn less_than() {
|
||||
let mut ctx = ExprBuilder::new();
|
||||
|
||||
let a = Box::new(ctx.float_var("a".to_owned()).unwrap());
|
||||
let spec = Box::new(ctx.make_lt(a, Box::new(ctx.float_const(0.0))));
|
||||
|
||||
let signals = HashMap::from_iter(vec![(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 1.3),
|
||||
(Duration::from_secs_f64(0.7), 3.0),
|
||||
(Duration::from_secs_f64(1.3), 0.1),
|
||||
(Duration::from_secs_f64(2.1), -2.2),
|
||||
])) as Box<dyn AnySignal>,
|
||||
)]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
|
||||
let rob = QuantitativeSemantics::eval::<Linear>(&spec, &trace).unwrap();
|
||||
let expected = Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 0.0 - 1.3),
|
||||
(Duration::from_secs_f64(0.7), 0.0 - 3.0),
|
||||
(Duration::from_secs_f64(1.3), 0.0 - 0.1),
|
||||
(Duration::from_secs_f64(1.334782609), 0.0), // interpolated at
|
||||
(Duration::from_secs_f64(2.1), 0.0 - (-2.2)),
|
||||
]);
|
||||
|
||||
assert_approx_eq(&rob, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eventually_unbounded() {
|
||||
let mut ctx = ExprBuilder::new();
|
||||
|
||||
let a = Box::new(ctx.float_var("a".to_owned()).unwrap());
|
||||
let cmp = Box::new(ctx.make_ge(a, Box::new(ctx.float_const(0.0))));
|
||||
let spec = ctx.make_eventually(cmp);
|
||||
|
||||
{
|
||||
let signals = HashMap::from_iter(vec![(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 2.5),
|
||||
(Duration::from_secs_f64(0.7), 4.0),
|
||||
(Duration::from_secs_f64(1.3), -1.0),
|
||||
(Duration::from_secs_f64(2.1), 1.7),
|
||||
])) as Box<dyn AnySignal>,
|
||||
)]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
let rob = QuantitativeSemantics::eval::<Linear>(&spec, &trace).unwrap();
|
||||
println!("{:#?}", rob);
|
||||
let expected = Signal::from_iter(vec![
|
||||
(Duration::from_secs_f64(0.0), 4.0),
|
||||
(Duration::from_secs_f64(0.7), 4.0),
|
||||
(Duration::from_secs_f64(1.18), 1.7),
|
||||
(Duration::from_secs_f64(1.3), 1.7),
|
||||
(Duration::from_secs_f64(1.596296296), 1.7), // interpolated at
|
||||
(Duration::from_secs_f64(2.1), 1.7),
|
||||
]);
|
||||
|
||||
assert_equal(&rob, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbounded_until() {
|
||||
let mut ctx = ExprBuilder::new();
|
||||
|
||||
let a = Box::new(ctx.int_var("a".to_owned()).unwrap());
|
||||
let b = Box::new(ctx.int_var("b".to_owned()).unwrap());
|
||||
let lhs = Box::new(ctx.make_gt(a, Box::new(ctx.int_const(0))));
|
||||
let rhs = Box::new(ctx.make_gt(b, Box::new(ctx.int_const(0))));
|
||||
let spec = ctx.make_until(lhs, rhs);
|
||||
|
||||
{
|
||||
let signals = HashMap::from_iter(vec![
|
||||
(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::<i64>::from_iter(vec![
|
||||
(Duration::from_secs(0), 2),
|
||||
(Duration::from_secs(5), 2),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
(
|
||||
"b".to_owned(),
|
||||
Box::new(Signal::<i64>::from_iter(vec![
|
||||
(Duration::from_secs(0), 4),
|
||||
(Duration::from_secs(5), 4),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
let rob = QuantitativeSemantics::eval::<Constant>(&spec, &trace).unwrap();
|
||||
|
||||
let expected = Signal::from_iter(vec![(Duration::from_secs(0), 2), (Duration::from_secs(5), 2)])
|
||||
.num_cast::<f64>()
|
||||
.unwrap();
|
||||
|
||||
assert_equal(&rob, &expected);
|
||||
}
|
||||
|
||||
{
|
||||
let signals = HashMap::from_iter(vec![
|
||||
(
|
||||
"a".to_owned(),
|
||||
Box::new(Signal::<i64>::from_iter(vec![
|
||||
(Duration::from_secs_f64(1.0), 1),
|
||||
(Duration::from_secs_f64(3.5), 7),
|
||||
(Duration::from_secs_f64(4.7), 3),
|
||||
(Duration::from_secs_f64(5.3), 5),
|
||||
(Duration::from_secs_f64(6.2), 1),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
(
|
||||
"b".to_owned(),
|
||||
Box::new(Signal::<i64>::from_iter(vec![
|
||||
(Duration::from_secs(4), 2),
|
||||
(Duration::from_secs(6), 3),
|
||||
])) as Box<dyn AnySignal>,
|
||||
),
|
||||
]);
|
||||
|
||||
let trace = MyTrace { signals };
|
||||
let rob = QuantitativeSemantics::eval::<Constant>(&spec, &trace).unwrap();
|
||||
|
||||
let expected = Signal::from_iter(vec![(Duration::from_secs(4), 3), (Duration::from_secs(6), 3)])
|
||||
.num_cast::<f64>()
|
||||
.unwrap();
|
||||
|
||||
assert_equal(&rob, &expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
0
argus/src/semantics/traits.rs
Normal file
0
argus/src/semantics/traits.rs
Normal file
1
argus/src/semantics/utils.rs
Normal file
1
argus/src/semantics/utils.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod lemire_minmax;
|
||||
130
argus/src/semantics/utils/lemire_minmax.rs
Normal file
130
argus/src/semantics/utils/lemire_minmax.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//! A Rusty implementation of Alex Donze's adaptation[^1] of Daniel Lemire's streaming
|
||||
//! min-max algorithm[^2] for piecewise linear signals.
|
||||
//!
|
||||
//! [^1]: Alexandre Donzé, Thomas Ferrère, and Oded Maler. 2013. Efficient Robust
|
||||
//! Monitoring for STL. In Computer Aided Verification (Lecture Notes in Computer
|
||||
//! Science), Springer, Berlin, Heidelberg, 264–279.
|
||||
//!
|
||||
//! [^2]: Daniel Lemire. 2007. Streaming Maximum-Minimum Filter Using No More than Three
|
||||
//! Comparisons per Element. arXiv:cs/0610046.
|
||||
|
||||
// TODO: Make a MonoWedge iterator adapter.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MonoWedge<'a, T> {
|
||||
window: VecDeque<(&'a Duration, &'a T)>,
|
||||
duration: Duration,
|
||||
cmp: fn(&T, &T) -> bool,
|
||||
}
|
||||
|
||||
impl<'a, T> MonoWedge<'a, T> {
|
||||
pub fn new(duration: Duration, cmp: fn(&T, &T) -> bool) -> Self {
|
||||
Self {
|
||||
window: Default::default(),
|
||||
cmp,
|
||||
duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> MonoWedge<'a, T> {
|
||||
pub fn update(&mut self, sample: (&'a Duration, &'a T)) {
|
||||
assert!(
|
||||
self.window.back().map_or(true, |v| v.0 < sample.0),
|
||||
"MonoWedge window samples don't have monotonic time"
|
||||
);
|
||||
// Find the index to partition the inner queue based on the comparison function.
|
||||
let cmp_idx = self.window.partition_point(|a| (self.cmp)(a.1, sample.1));
|
||||
assert!(cmp_idx <= self.window.len());
|
||||
|
||||
// And delete all items in the second partition.
|
||||
let _ = self.window.split_off(cmp_idx);
|
||||
|
||||
// Clear all older values
|
||||
while let Some(item) = self.window.front() {
|
||||
if *sample.0 > self.duration + *item.0 {
|
||||
let _ = self.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new value
|
||||
self.window.push_back(sample);
|
||||
}
|
||||
|
||||
pub fn front(&self) -> Option<(&'a Duration, &'a T)> {
|
||||
self.window.front().copied()
|
||||
}
|
||||
|
||||
pub fn pop_front(&mut self) -> Option<(&'a Duration, &'a T)> {
|
||||
self.window.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> MonoWedge<'a, T>
|
||||
where
|
||||
T: PartialOrd,
|
||||
{
|
||||
#[allow(dead_code)]
|
||||
pub fn min_wedge(duration: Duration) -> Self {
|
||||
Self::new(duration, T::lt)
|
||||
}
|
||||
|
||||
pub fn max_wedge(duration: Duration) -> Self {
|
||||
Self::new(duration, T::gt)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
prop_compose! {
|
||||
fn vec_and_window()(vec in prop::collection::vec(any::<u64>(), 3..100))
|
||||
(window_size in 2..vec.len(), vec in Just(vec))
|
||||
-> (Vec<u64>, usize) {
|
||||
(vec, window_size)
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_rolling_minmax((vec, width) in vec_and_window()) {
|
||||
// NOTE: When we convert the width from usize to Duration, the window becomes inclusive.
|
||||
let expected_mins: Vec<u64> = vec.as_slice().windows(width + 1).map(|w| w.iter().min().unwrap_or_else(|| panic!("slice should have min: {:?}", w))).copied().collect();
|
||||
assert_eq!(expected_mins.len(), vec.len() - width);
|
||||
let expected_maxs: Vec<u64> = vec.as_slice().windows(width + 1).map(|w| w.iter().max().unwrap_or_else(|| panic!("slice should have max: {:?}", w))).copied().collect();
|
||||
assert_eq!(expected_maxs.len(), vec.len() - width);
|
||||
|
||||
let time_points: Vec<Duration> = (0..vec.len()).map(|i| Duration::from_secs(i as u64)).collect();
|
||||
let width = Duration::from_secs(width as u64);
|
||||
|
||||
let mut min_wedge = MonoWedge::<u64>::min_wedge(width);
|
||||
let mut max_wedge = MonoWedge::<u64>::max_wedge(width);
|
||||
let mut ret_mins = Vec::with_capacity(expected_mins.len());
|
||||
let mut ret_maxs = Vec::with_capacity(expected_maxs.len());
|
||||
|
||||
// Now we do the actual updates
|
||||
for (i, value) in time_points.iter().zip(&vec) {
|
||||
min_wedge.update((i, value));
|
||||
max_wedge.update((i, value));
|
||||
if i >= &(time_points[0] + width) {
|
||||
ret_mins.push(min_wedge.front().unwrap_or_else(|| panic!("min_wedge should have at least 1 element")));
|
||||
ret_maxs.push(max_wedge.front().unwrap_or_else(|| panic!("max_wedge should have at least 1 element")))
|
||||
}
|
||||
}
|
||||
|
||||
let ret_mins: Vec<_> = ret_mins.into_iter().map(|s| s.1).copied().collect();
|
||||
let ret_maxs: Vec<_> = ret_maxs.into_iter().map(|s| s.1).copied().collect();
|
||||
assert_eq!(expected_mins, ret_mins, "window min incorrect");
|
||||
assert_eq!(expected_maxs, ret_maxs, "window max incorrect");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue