use scirs2_core::ndarray::Array1;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Clone)]
pub enum LTLFormula {
Atomic(fn(&Array1<f32>) -> bool),
Not(Box<LTLFormula>),
And(Box<LTLFormula>, Box<LTLFormula>),
Or(Box<LTLFormula>, Box<LTLFormula>),
Next(Box<LTLFormula>),
Always(Box<LTLFormula>),
Eventually(Box<LTLFormula>),
Until(Box<LTLFormula>, Box<LTLFormula>),
}
impl LTLFormula {
pub fn check(&self, trace: &[Array1<f32>], position: usize) -> bool {
match self {
LTLFormula::Atomic(pred) => {
if position < trace.len() {
pred(&trace[position])
} else {
false
}
}
LTLFormula::Not(phi) => !phi.check(trace, position),
LTLFormula::And(phi1, phi2) => {
phi1.check(trace, position) && phi2.check(trace, position)
}
LTLFormula::Or(phi1, phi2) => {
phi1.check(trace, position) || phi2.check(trace, position)
}
LTLFormula::Next(phi) => {
if position + 1 < trace.len() {
phi.check(trace, position + 1)
} else {
false
}
}
LTLFormula::Always(phi) => {
(position..trace.len()).all(|i| phi.check(trace, i))
}
LTLFormula::Eventually(phi) => {
(position..trace.len()).any(|i| phi.check(trace, i))
}
LTLFormula::Until(phi1, phi2) => {
for i in position..trace.len() {
if phi2.check(trace, i) {
return true;
}
if !phi1.check(trace, i) {
return false;
}
}
false
}
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TemporalBound {
pub lower: f32,
pub upper: f32,
}
impl TemporalBound {
pub fn new(lower: f32, upper: f32) -> Self {
Self { lower, upper }
}
pub fn contains(&self, time: f32) -> bool {
time >= self.lower && time <= self.upper
}
}
#[derive(Clone)]
pub enum STLFormula {
Predicate(fn(&Array1<f32>) -> f32),
Not(Box<STLFormula>),
And(Box<STLFormula>, Box<STLFormula>),
Or(Box<STLFormula>, Box<STLFormula>),
Always {
formula: Box<STLFormula>,
bound: TemporalBound,
},
Eventually {
formula: Box<STLFormula>,
bound: TemporalBound,
},
Until {
phi1: Box<STLFormula>,
phi2: Box<STLFormula>,
bound: TemporalBound,
},
}
impl STLFormula {
pub fn robustness(&self, trace: &[Array1<f32>], time: f32) -> f32 {
let idx = time as usize;
match self {
STLFormula::Predicate(pred) => {
if idx < trace.len() {
pred(&trace[idx])
} else {
f32::NEG_INFINITY
}
}
STLFormula::Not(phi) => -phi.robustness(trace, time),
STLFormula::And(phi1, phi2) => phi1
.robustness(trace, time)
.min(phi2.robustness(trace, time)),
STLFormula::Or(phi1, phi2) => phi1
.robustness(trace, time)
.max(phi2.robustness(trace, time)),
STLFormula::Always { formula, bound } => {
let start = (time + bound.lower) as usize;
let end = (time + bound.upper) as usize;
let end = end.min(trace.len());
(start..end)
.map(|i| formula.robustness(trace, i as f32))
.fold(f32::INFINITY, f32::min)
}
STLFormula::Eventually { formula, bound } => {
let start = (time + bound.lower) as usize;
let end = (time + bound.upper) as usize;
let end = end.min(trace.len());
(start..end)
.map(|i| formula.robustness(trace, i as f32))
.fold(f32::NEG_INFINITY, f32::max)
}
STLFormula::Until { phi1, phi2, bound } => {
let start = (time + bound.lower) as usize;
let end = (time + bound.upper) as usize;
let end = end.min(trace.len());
let mut max_rob = f32::NEG_INFINITY;
for i in start..end {
let rob2 = phi2.robustness(trace, i as f32);
let min_rob1 = (start..i)
.map(|j| phi1.robustness(trace, j as f32))
.fold(f32::INFINITY, f32::min);
max_rob = max_rob.max(rob2.min(min_rob1));
}
max_rob
}
}
}
pub fn is_satisfied(&self, trace: &[Array1<f32>], time: f32) -> bool {
self.robustness(trace, time) >= 0.0
}
}
pub struct TemporalConstraintEnforcer {
ltl_formulas: Vec<LTLFormula>,
stl_formulas: Vec<STLFormula>,
trace: VecDeque<Array1<f32>>,
max_trace_len: usize,
current_time: usize,
}
impl TemporalConstraintEnforcer {
pub fn new(max_trace_len: usize) -> Self {
Self {
ltl_formulas: Vec::new(),
stl_formulas: Vec::new(),
trace: VecDeque::new(),
max_trace_len,
current_time: 0,
}
}
pub fn add_ltl(&mut self, formula: LTLFormula) {
self.ltl_formulas.push(formula);
}
pub fn add_stl(&mut self, formula: STLFormula) {
self.stl_formulas.push(formula);
}
pub fn update(&mut self, signal: Array1<f32>) {
self.trace.push_back(signal);
if self.trace.len() > self.max_trace_len {
self.trace.pop_front();
}
self.current_time += 1;
}
pub fn check_ltl(&self) -> bool {
if self.trace.is_empty() {
return true;
}
let trace_vec: Vec<_> = self.trace.iter().cloned().collect();
self.ltl_formulas
.iter()
.all(|formula| formula.check(&trace_vec, 0))
}
pub fn check_stl(&self) -> bool {
if self.trace.is_empty() {
return true;
}
let trace_vec: Vec<_> = self.trace.iter().cloned().collect();
let latest_time = (trace_vec.len() - 1) as f32;
self.stl_formulas
.iter()
.all(|formula| formula.is_satisfied(&trace_vec, latest_time))
}
pub fn stl_robustness(&self) -> Vec<f32> {
if self.trace.is_empty() {
return vec![];
}
let trace_vec: Vec<_> = self.trace.iter().cloned().collect();
let latest_time = (trace_vec.len() - 1) as f32;
self.stl_formulas
.iter()
.map(|formula| formula.robustness(&trace_vec, latest_time))
.collect()
}
pub fn check_all(&self) -> bool {
self.check_ltl() && self.check_stl()
}
pub fn reset(&mut self) {
self.trace.clear();
self.current_time = 0;
}
pub fn trace_length(&self) -> usize {
self.trace.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ltl_atomic() {
let positive = LTLFormula::Atomic(|x| x[0] > 0.0);
let trace = vec![
Array1::from_vec(vec![1.0]),
Array1::from_vec(vec![2.0]),
Array1::from_vec(vec![3.0]),
];
assert!(positive.check(&trace, 0));
assert!(positive.check(&trace, 1));
assert!(positive.check(&trace, 2));
let trace_neg = vec![
Array1::from_vec(vec![1.0]),
Array1::from_vec(vec![-1.0]),
Array1::from_vec(vec![2.0]),
];
assert!(!positive.check(&trace_neg, 1));
}
#[test]
fn test_ltl_always() {
let always_positive = LTLFormula::Always(Box::new(LTLFormula::Atomic(|x| x[0] > 0.0)));
let trace_good = vec![
Array1::from_vec(vec![1.0]),
Array1::from_vec(vec![2.0]),
Array1::from_vec(vec![3.0]),
];
assert!(always_positive.check(&trace_good, 0));
let trace_bad = vec![
Array1::from_vec(vec![1.0]),
Array1::from_vec(vec![-1.0]),
Array1::from_vec(vec![2.0]),
];
assert!(!always_positive.check(&trace_bad, 0));
}
#[test]
fn test_ltl_eventually() {
let eventually_large =
LTLFormula::Eventually(Box::new(LTLFormula::Atomic(|x| x[0] > 10.0)));
let trace = vec![
Array1::from_vec(vec![1.0]),
Array1::from_vec(vec![5.0]),
Array1::from_vec(vec![15.0]),
];
assert!(eventually_large.check(&trace, 0));
let trace_bad = vec![
Array1::from_vec(vec![1.0]),
Array1::from_vec(vec![5.0]),
Array1::from_vec(vec![9.0]),
];
assert!(!eventually_large.check(&trace_bad, 0));
}
#[test]
fn test_stl_predicate() {
let pred = STLFormula::Predicate(|x| x[0] - 5.0);
let trace = vec![Array1::from_vec(vec![10.0]), Array1::from_vec(vec![3.0])];
assert_eq!(pred.robustness(&trace, 0.0), 5.0);
assert_eq!(pred.robustness(&trace, 1.0), -2.0);
assert!(pred.is_satisfied(&trace, 0.0));
assert!(!pred.is_satisfied(&trace, 1.0));
}
#[test]
fn test_stl_always() {
let always_large = STLFormula::Always {
formula: Box::new(STLFormula::Predicate(|x| x[0] - 5.0)),
bound: TemporalBound::new(0.0, 3.0),
};
let trace_good = vec![
Array1::from_vec(vec![10.0]),
Array1::from_vec(vec![8.0]),
Array1::from_vec(vec![7.0]),
];
assert_eq!(always_large.robustness(&trace_good, 0.0), 2.0);
let trace_bad = vec![
Array1::from_vec(vec![10.0]),
Array1::from_vec(vec![3.0]),
Array1::from_vec(vec![8.0]),
];
assert_eq!(always_large.robustness(&trace_bad, 0.0), -2.0);
}
#[test]
fn test_temporal_bound() {
let bound = TemporalBound::new(2.0, 5.0);
assert!(!bound.contains(1.0));
assert!(bound.contains(2.0));
assert!(bound.contains(3.5));
assert!(bound.contains(5.0));
assert!(!bound.contains(6.0));
}
#[test]
fn test_temporal_enforcer() {
let mut enforcer = TemporalConstraintEnforcer::new(10);
let always_positive = LTLFormula::Always(Box::new(LTLFormula::Atomic(|x| x[0] > 0.0)));
enforcer.add_ltl(always_positive);
enforcer.update(Array1::from_vec(vec![1.0]));
enforcer.update(Array1::from_vec(vec![2.0]));
enforcer.update(Array1::from_vec(vec![3.0]));
assert!(enforcer.check_ltl());
assert_eq!(enforcer.trace_length(), 3);
enforcer.update(Array1::from_vec(vec![-1.0]));
assert!(!enforcer.check_ltl());
enforcer.reset();
assert_eq!(enforcer.trace_length(), 0);
}
#[test]
fn test_stl_enforcer() {
let mut enforcer = TemporalConstraintEnforcer::new(10);
let predicate = STLFormula::Predicate(|x| x[0] - 5.0);
enforcer.add_stl(predicate);
enforcer.update(Array1::from_vec(vec![10.0]));
assert!(enforcer.check_stl());
enforcer.update(Array1::from_vec(vec![3.0]));
assert!(!enforcer.check_stl());
let robustness = enforcer.stl_robustness();
assert_eq!(robustness.len(), 1);
}
}