banquo_core/lib.rs
1#![deny(clippy::all)]
2
3use std::borrow::Borrow;
4use std::sync::Arc;
5use std::rc::Rc;
6
7use thiserror::Error;
8
9pub mod metrics;
10pub mod operators;
11pub mod predicate;
12pub mod trace;
13
14pub use crate::trace::Trace;
15pub use crate::metrics::{Top, Bottom, Meet, Join};
16
17/// Trait representing a temporal logic formula that can evaluate a timed set of system states
18/// called a [`Trace`].
19///
20/// In general, temporal logic formulas are used to represent requirements over the behaviors of a
21/// system, and are evaluated to quantify the degree to which the system satisfies or violates the
22/// requirement. There are many types of metrics used for this analysis, but this library defaults
23/// to [robustness] which is simply a measure of distance representing how "close" the system
24/// came to violation.
25///
26/// Implementations of this trait are expected to maintain the length of the trace, and should be
27/// able to handle input traces with no elements.
28///
29/// # Examples
30///
31/// For additional examples, please refer to the [`Predicate`](crate::predicate::Predicate) type and the
32/// [`operators`](crate::operators) module.
33///
34/// [robustness]: https://link.springer.com/chapter/10.1007/11940197_12
35///
36/// ```rust
37/// # use banquo::{Formula, Trace};
38/// struct UnitFormula;
39///
40/// impl<T> Formula<T> for UnitFormula {
41/// type Metric = ();
42/// type Error = ();
43///
44/// fn evaluate(&self, trace: &Trace<T>) -> Result<Trace<Self::Metric>, Self::Error> {
45/// let result = trace
46/// .iter()
47/// .map_states(|_| ())
48/// .collect();
49///
50/// Ok(result)
51/// }
52/// }
53/// ```
54pub trait Formula<State> {
55 /// The output metric from this formula
56 type Metric;
57
58 /// The type of error that may be produced during evaluation
59 type Error;
60
61 /// Evaluate a given trace of system states into either a trace of metric values or an error.
62 fn evaluate(&self, trace: &Trace<State>) -> Result<Trace<Self::Metric>, Self::Error>;
63}
64
65impl<State, T> Formula<State> for &T
66where
67 T: Formula<State> + ?Sized,
68{
69 type Metric = T::Metric;
70 type Error = T::Error;
71
72 fn evaluate(&self, trace: &Trace<State>) -> Result<Trace<Self::Metric>, Self::Error> {
73 (**self).evaluate(trace)
74 }
75}
76
77impl<State, T> Formula<State> for Box<T>
78where
79 T: Formula<State> + ?Sized,
80{
81 type Metric = T::Metric;
82 type Error = T::Error;
83
84 fn evaluate(&self, trace: &Trace<State>) -> Result<Trace<Self::Metric>, Self::Error> {
85 (**self).evaluate(trace)
86 }
87}
88
89impl<State, T> Formula<State> for Arc<T>
90where
91 T: Formula<State> + ?Sized,
92{
93 type Metric = T::Metric;
94 type Error = T::Error;
95
96 fn evaluate(&self, trace: &Trace<State>) -> Result<Trace<Self::Metric>, Self::Error> {
97 (**self).evaluate(trace)
98 }
99}
100
101impl<State, T> Formula<State> for Rc<T>
102where
103 T: Formula<State> + ?Sized,
104{
105 type Metric = T::Metric;
106 type Error = T::Error;
107
108 fn evaluate(&self, trace: &Trace<State>) -> Result<Trace<Self::Metric>, Self::Error> {
109 (**self).evaluate(trace)
110 }
111}
112
113#[derive(Debug, Error)]
114enum ErrorKind {
115 #[error("Error evaluating formula: {source}")]
116 FormulaError {
117 source: Box<dyn std::error::Error>,
118 },
119
120 #[error("Empty trace")]
121 EmptyTraceError,
122}
123
124/// Error produced while evaluating a [`Trace`] using the [`evaluate`] function.
125///
126/// Two conditions are represented using this error type:
127/// 1. An error is encountered during evaluation of the formula
128/// 2. The trace does not contain any elements.
129#[derive(Debug, Error)]
130#[error(transparent)]
131pub struct EvaluationError(ErrorKind);
132
133impl EvaluationError {
134 /// Create a new evaluation error for an arbitrary inner error.
135 ///
136 /// The error provided this function is expected to be generated from a [`Formula`] evaluation.
137 /// This function allocates on the heap.
138 pub fn wrap<E>(source: E) -> Self
139 where
140 E: std::error::Error + 'static,
141 {
142 Self(ErrorKind::FormulaError { source: source.into() })
143 }
144
145 /// Create a new evaluation error when a trace does not contain any elements.
146 pub fn empty() -> Self {
147 Self(ErrorKind::EmptyTraceError)
148 }
149}
150
151/// Evaluate a [`Trace`] using a given [`Formula`] and return the metric value for the earliest
152/// time.
153///
154/// This function accepts either a borrowed or owned trace value, and any [`Formula`] type. The
155/// trace is evaluated using the formula, which may produce an error, and then the first
156/// metric is extracted from the trace , which will also produce an error if the trace is empty.
157///
158/// # Examples
159///
160/// ```rust
161/// use std::collections::HashMap;
162///
163/// use banquo::{EvaluationError, Trace, evaluate, predicate};
164///
165/// let phi = predicate!{ x <= 10.0 };
166///
167/// let t1 = Trace::from([
168/// (0.0, HashMap::from([("x", 8.0)])),
169/// (1.0, HashMap::from([("x", 4.0)])),
170/// ]);
171///
172/// let t2 = Trace::from([
173/// (0.0, HashMap::from([("y", 8.0)])),
174/// ]);
175///
176/// let t3: Trace<HashMap<&'static str, f64>> = Trace::new();
177///
178/// evaluate(&t1, &phi); // Ok -> 2.0
179/// evaluate(&t2, &phi); // Error -> predicate evaluation error
180/// evaluate(t3, &phi); // Error -> empty trace
181/// ```
182pub fn evaluate<T, F, State, M, E>(trace: T, formula: F) -> Result<M, EvaluationError>
183where
184 T: Borrow<Trace<State>>,
185 F: Formula<State, Metric = M, Error = E>,
186 E: std::error::Error + 'static,
187{
188 formula
189 .evaluate(trace.borrow())
190 .map_err(EvaluationError::wrap)
191 .and_then(|trace| {
192 trace
193 .into_iter()
194 .next()
195 .map(|(_, metric)| metric)
196 .ok_or_else(EvaluationError::empty)
197 })
198}