Skip to main content

taco_threshold_automaton/lia_threshold_automaton/general_to_lia/
classify_into_lia.rs

1//! This module contains the logic to transform a general boolean expression
2//! over variables into a [`super::LIAVariableConstraint`].
3//!
4//! The conversion generally works as described in
5//! [`../general_to_lia.rs`](general_to_lia.rs).
6//!
7//! This module implements the logic that after successfully splitting the
8//! expression into pairs of atoms and rational factors, brings all variables to
9//! the left and collects all parameter factor pairs and constants on the right.
10//!
11//! Then the module converts them into the appropriate [`LIAVariableConstraint`]
12//! type, i.e., either a [`SingleAtomConstraint`], [`SumAtomConstraint`] or
13//! [`ComparisonConstraint`].
14
15use std::{collections::HashMap, hash::Hash};
16
17use crate::{
18    BooleanVarConstraint,
19    expressions::{
20        Atomic, BooleanConnective, BooleanExpression, IntegerExpression, Variable,
21        fraction::Fraction,
22    },
23    lia_threshold_automaton::{
24        ComparisonConstraint, LIAVariableConstraint, SingleAtomConstraint, SumAtomConstraint,
25        integer_thresholds::{
26            DeriveFromIntegerComp, IntoNoDivBooleanExpr, Threshold, ThresholdCompOp,
27            ThresholdConstraint,
28        },
29    },
30};
31
32use super::{ConstraintRewriteError, remove_boolean_neg::NonNegatedBooleanExpression};
33
34impl TryFrom<&BooleanVarConstraint> for LIAVariableConstraint {
35    type Error = ConstraintRewriteError;
36
37    fn try_from(expr: &BooleanExpression<Variable>) -> Result<Self, Self::Error> {
38        let non_negated = expr.remove_boolean_negations();
39        non_negated.try_into()
40    }
41}
42
43impl TryFrom<BooleanVarConstraint> for LIAVariableConstraint {
44    type Error = ConstraintRewriteError;
45
46    fn try_from(expr: BooleanExpression<Variable>) -> Result<Self, Self::Error> {
47        (&expr).try_into()
48    }
49}
50
51impl TryFrom<NonNegatedBooleanExpression<Variable>> for LIAVariableConstraint {
52    type Error = ConstraintRewriteError;
53    /// Parse the `NonNegatedBooleanExpression` into a LIAVariableConstraint by
54    /// converting into weighted sums of parameters and variables
55    fn try_from(value: NonNegatedBooleanExpression<Variable>) -> Result<Self, Self::Error> {
56        match value {
57            NonNegatedBooleanExpression::True => Ok(LIAVariableConstraint::True),
58            NonNegatedBooleanExpression::False => Ok(LIAVariableConstraint::False),
59            NonNegatedBooleanExpression::BinaryExpression(lhs, op, rhs) => {
60                let lhs: LIAVariableConstraint = (*lhs).try_into()?;
61                let rhs = (*rhs).try_into()?;
62
63                // Simplification
64                if lhs.is_top() {
65                    if op == BooleanConnective::And {
66                        return Ok(rhs);
67                    }
68                    return Ok(LIAVariableConstraint::True);
69                }
70                if rhs.is_top() {
71                    if op == BooleanConnective::And {
72                        return Ok(lhs);
73                    }
74                    return Ok(LIAVariableConstraint::True);
75                }
76
77                Ok(LIAVariableConstraint::BinaryGuard(
78                    Box::new(lhs),
79                    op,
80                    Box::new(rhs),
81                ))
82            }
83            NonNegatedBooleanExpression::ComparisonExpression(lhs, op, rhs) => {
84                LIAVariableConstraint::from_integer_expr(*lhs, op, *rhs)
85            }
86        }
87    }
88}
89
90/// This function is designed to rewrite an comparison expression into a form
91/// where the returned `HashMap<T, Fraction>` forms the new lhs of the equation
92/// and the returned threshold is the right hand side of the equation
93pub(crate) fn split_pairs_into_atom_and_threshold<T: Atomic>(
94    lhs: IntegerExpression<T>,
95    rhs: IntegerExpression<T>,
96) -> Result<(HashMap<T, Fraction>, Threshold), ConstraintRewriteError> {
97    let lhs = lhs
98        .remove_unary_neg_and_sub()
99        .remove_div()?
100        .split_into_factor_pairs()?;
101
102    let rhs = rhs
103        .remove_unary_neg_and_sub()
104        .remove_div()?
105        .split_into_factor_pairs()?;
106
107    let atoms = combine_pair_iterators(lhs.get_atom_factor_pairs(), rhs.get_atom_factor_pairs());
108
109    let params = combine_pair_iterators(rhs.get_param_factor_pairs(), lhs.get_param_factor_pairs());
110
111    let c = rhs.get_const_factor() - lhs.get_const_factor();
112    let t = Threshold::new(params, c);
113
114    Ok((atoms, t))
115}
116
117impl DeriveFromIntegerComp<Variable, ConstraintRewriteError> for LIAVariableConstraint {
118    fn form_ordered_constr(
119        scaled_t: HashMap<Variable, Fraction>,
120        thr_c: ThresholdConstraint,
121    ) -> Result<Self, ConstraintRewriteError> {
122        if scaled_t.is_empty() {
123            // Constraint of the form `0 <op> c`
124            if let Some(c) = thr_c.get_threshold().get_const() {
125                let zero = Fraction::from(0);
126                let holds = match thr_c.get_op() {
127                    ThresholdCompOp::Geq => zero >= c,
128                    ThresholdCompOp::Gt => zero > c,
129                    ThresholdCompOp::Lt => zero < c,
130                    ThresholdCompOp::Leq => zero <= c,
131                };
132
133                if holds {
134                    return Ok(LIAVariableConstraint::True);
135                }
136                return Ok(LIAVariableConstraint::False);
137            }
138
139            // The evaluation will fail if the constraint contains parameters
140            // This would mean that this expression constrains the parameters
141            // which is only permitted inside the resilience condition
142            return Err(ConstraintRewriteError::ParameterConstraint(Box::new(
143                BooleanExpression::ComparisonExpression(
144                    Box::new(IntegerExpression::Const(0)),
145                    thr_c.get_op().into(),
146                    Box::new(thr_c.get_threshold().get_scaled_integer_expression(1)),
147                ),
148            )));
149        }
150
151        if scaled_t.len() == 1 {
152            let (var, factor) = scaled_t.into_iter().next().unwrap();
153            let mut thr = thr_c;
154            thr.scale(factor.inverse());
155
156            let svar_guard = SingleAtomConstraint::new(var, thr);
157
158            return Ok(LIAVariableConstraint::SingleVarConstraint(svar_guard));
159        }
160
161        if let Ok(g) = SumAtomConstraint::try_new(scaled_t.clone(), thr_c.clone()) {
162            return Ok(LIAVariableConstraint::SumVarConstraint(g));
163        }
164
165        Ok(LIAVariableConstraint::ComparisonConstraint(
166            ComparisonConstraint::try_new(scaled_t, thr_c)
167                .expect("Failed to categorize guard type."),
168        ))
169    }
170}
171
172/// Combine two iterators over pairs of tuples over (T, Fraction) into a single
173/// map from T to [`Fraction`], where all entries from `negated_it` are inserted
174/// negated
175fn combine_pair_iterators<'a, T: Clone + Eq + Hash + 'a>(
176    reg_it: impl Iterator<Item = (&'a T, &'a Fraction)>,
177    negated_it: impl Iterator<Item = (&'a T, &'a Fraction)>,
178) -> HashMap<T, Fraction> {
179    reg_it
180        .map(|(t, f)| (t.clone(), *f))
181        // negate the all factors from `negated_it` and add them to the chain
182        .chain(negated_it.map(|(t, f)| (t.clone(), -*f)))
183        .fold(HashMap::new(), |mut acc, (p, f)| {
184            // if key exists, add the factor, otherwise insert it
185            acc.entry(p).and_modify(|e| *e += f).or_insert(f);
186
187            acc
188        })
189}
190
191#[cfg(test)]
192mod tests {
193    use std::collections::BTreeMap;
194
195    use crate::{
196        expressions::{
197            BooleanConnective, ComparisonOp, IntegerExpression, Parameter, Variable,
198            fraction::Fraction,
199        },
200        lia_threshold_automaton::{
201            ConstraintRewriteError::{self},
202            LIAVariableConstraint, SingleAtomConstraint, SumAtomConstraint,
203            general_to_lia::remove_boolean_neg::NonNegatedBooleanExpression,
204            integer_thresholds::{ThresholdCompOp, ThresholdConstraint},
205        },
206    };
207
208    #[test]
209    fn test_try_into_single_var() {
210        let expr = NonNegatedBooleanExpression::ComparisonExpression(
211            Box::new(IntegerExpression::Atom(Variable::new("var"))),
212            ComparisonOp::Gt,
213            Box::new(IntegerExpression::Const(0)),
214        );
215
216        let expected_expr = LIAVariableConstraint::SingleVarConstraint(SingleAtomConstraint::new(
217            Variable::new("var"),
218            ThresholdConstraint::new(ThresholdCompOp::Gt, Vec::<(Parameter, Fraction)>::new(), 0),
219        ));
220        let got = LIAVariableConstraint::try_from(expr).unwrap();
221        assert_eq!(got, expected_expr);
222    }
223
224    #[test]
225    fn test_try_into_single_var_eq() {
226        // var == 0
227        let expr = NonNegatedBooleanExpression::ComparisonExpression(
228            Box::new(IntegerExpression::Atom(Variable::new("var"))),
229            ComparisonOp::Eq,
230            Box::new(IntegerExpression::Const(0)),
231        );
232
233        // var =< 0 || var >= 0
234        let expected_expr = LIAVariableConstraint::BinaryGuard(
235            Box::new(LIAVariableConstraint::SingleVarConstraint(
236                SingleAtomConstraint::new(
237                    Variable::new("var"),
238                    ThresholdConstraint::new(
239                        ThresholdCompOp::Leq,
240                        Vec::<(Parameter, Fraction)>::new(),
241                        0,
242                    ),
243                ),
244            )),
245            BooleanConnective::And,
246            Box::new(LIAVariableConstraint::SingleVarConstraint(
247                SingleAtomConstraint::new(
248                    Variable::new("var"),
249                    ThresholdConstraint::new(
250                        ThresholdCompOp::Geq,
251                        Vec::<(Parameter, Fraction)>::new(),
252                        0,
253                    ),
254                ),
255            )),
256        );
257        let got = LIAVariableConstraint::try_from(expr).unwrap();
258        assert_eq!(got, expected_expr);
259    }
260
261    #[test]
262    fn test_try_into_single_var_neq() {
263        // var != 1
264        let expr = NonNegatedBooleanExpression::ComparisonExpression(
265            Box::new(IntegerExpression::Atom(Variable::new("var"))),
266            ComparisonOp::Neq,
267            Box::new(IntegerExpression::Const(1)),
268        );
269
270        // var < 1 || var > 1
271        let expected_expr = LIAVariableConstraint::BinaryGuard(
272            Box::new(LIAVariableConstraint::SingleVarConstraint(
273                SingleAtomConstraint::new(
274                    Variable::new("var"),
275                    ThresholdConstraint::new(
276                        ThresholdCompOp::Lt,
277                        Vec::<(Parameter, Fraction)>::new(),
278                        1,
279                    ),
280                ),
281            )),
282            BooleanConnective::Or,
283            Box::new(LIAVariableConstraint::SingleVarConstraint(
284                SingleAtomConstraint::new(
285                    Variable::new("var"),
286                    ThresholdConstraint::new(
287                        ThresholdCompOp::Gt,
288                        Vec::<(Parameter, Fraction)>::new(),
289                        1,
290                    ),
291                ),
292            )),
293        );
294        let got = LIAVariableConstraint::try_from(expr).unwrap();
295        assert_eq!(got, expected_expr);
296    }
297
298    #[test]
299    fn test_try_into_single_var_scale_factor_correct() {
300        // 2 * var > 7
301        let expr = NonNegatedBooleanExpression::ComparisonExpression(
302            Box::new(IntegerExpression::Const(2) * IntegerExpression::Atom(Variable::new("var"))),
303            ComparisonOp::Gt,
304            Box::new(IntegerExpression::Const(7)),
305        );
306
307        let expected_expr = LIAVariableConstraint::SingleVarConstraint(SingleAtomConstraint::new(
308            Variable::new("var"),
309            ThresholdConstraint::new(
310                ThresholdCompOp::Gt,
311                Vec::<(Parameter, Fraction)>::new(),
312                Fraction::new(7, 2, false),
313            ),
314        ));
315        let got = LIAVariableConstraint::try_from(expr).unwrap();
316        assert_eq!(got, expected_expr);
317    }
318
319    #[test]
320    fn test_simple_guards() {
321        let expr: NonNegatedBooleanExpression<Variable> = NonNegatedBooleanExpression::True;
322        let expected = LIAVariableConstraint::True;
323
324        let result = LIAVariableConstraint::try_from(expr).unwrap();
325        assert_eq!(result, expected);
326
327        let expr: NonNegatedBooleanExpression<Variable> = NonNegatedBooleanExpression::False;
328        let expected = LIAVariableConstraint::False;
329
330        let result = LIAVariableConstraint::try_from(expr).unwrap();
331        assert_eq!(result, expected);
332    }
333
334    #[test]
335    fn test_binary_guard() {
336        // top && top
337        let expr = NonNegatedBooleanExpression::BinaryExpression(
338            Box::new(NonNegatedBooleanExpression::True),
339            BooleanConnective::And,
340            Box::new(NonNegatedBooleanExpression::True),
341        );
342
343        let expected_expr = LIAVariableConstraint::True;
344        let got = LIAVariableConstraint::try_from(expr).unwrap();
345        assert_eq!(got, expected_expr);
346
347        //bot || top
348        let expr = NonNegatedBooleanExpression::BinaryExpression(
349            Box::new(NonNegatedBooleanExpression::False),
350            BooleanConnective::Or,
351            Box::new(NonNegatedBooleanExpression::True),
352        );
353
354        let expected_expr = LIAVariableConstraint::True;
355        let got = LIAVariableConstraint::try_from(expr).unwrap();
356        assert_eq!(got, expected_expr);
357    }
358
359    #[test]
360    fn test_try_into_single_var_double_var_lhs() {
361        let expr = NonNegatedBooleanExpression::ComparisonExpression(
362            Box::new(
363                IntegerExpression::Atom(Variable::new("var"))
364                    + IntegerExpression::Atom(Variable::new("var")),
365            ),
366            ComparisonOp::Gt,
367            Box::new(IntegerExpression::Const(1)),
368        );
369
370        let expected_expr = LIAVariableConstraint::SingleVarConstraint(SingleAtomConstraint::new(
371            Variable::new("var"),
372            ThresholdConstraint::new(
373                ThresholdCompOp::Gt,
374                Vec::<(Parameter, Fraction)>::new(),
375                Fraction::new(1, 2, false),
376            ),
377        ));
378
379        let got = LIAVariableConstraint::try_from(expr).unwrap();
380        assert_eq!(got, expected_expr);
381    }
382
383    #[test]
384    fn test_try_into_single_var_double_var_rhs() {
385        let expr = NonNegatedBooleanExpression::ComparisonExpression(
386            Box::new(IntegerExpression::Const(1)),
387            ComparisonOp::Leq,
388            Box::new(
389                IntegerExpression::Atom(Variable::new("var"))
390                    + IntegerExpression::Atom(Variable::new("var")),
391            ),
392        );
393        let expected_expr = LIAVariableConstraint::SingleVarConstraint(SingleAtomConstraint::new(
394            Variable::new("var"),
395            ThresholdConstraint::new(
396                ThresholdCompOp::Geq,
397                Vec::<(Parameter, Fraction)>::new(),
398                Fraction::new(1, 2, false),
399            ),
400        ));
401        let got = LIAVariableConstraint::try_from(expr).unwrap();
402        assert_eq!(got, expected_expr);
403
404        // 1 < 2 var --> 2var > 1 --> var < 1/2
405        let expr = NonNegatedBooleanExpression::ComparisonExpression(
406            Box::new(IntegerExpression::Const(1)),
407            ComparisonOp::Lt,
408            Box::new(
409                IntegerExpression::Atom(Variable::new("var"))
410                    + IntegerExpression::Atom(Variable::new("var")),
411            ),
412        );
413        let expected_expr = LIAVariableConstraint::SingleVarConstraint(SingleAtomConstraint::new(
414            Variable::new("var"),
415            ThresholdConstraint::new(
416                ThresholdCompOp::Gt,
417                Vec::<(Parameter, Fraction)>::new(),
418                Fraction::new(1, 2, false),
419            ),
420        ));
421        let got = LIAVariableConstraint::try_from(expr).unwrap();
422        assert_eq!(got, expected_expr);
423
424        let expr = NonNegatedBooleanExpression::ComparisonExpression(
425            Box::new(IntegerExpression::Const(6) - IntegerExpression::Atom(Variable::new("var"))),
426            ComparisonOp::Leq,
427            Box::new(
428                IntegerExpression::Atom(Variable::new("var"))
429                    + IntegerExpression::Atom(Variable::new("var")),
430            ),
431        );
432        let expected_expr = LIAVariableConstraint::SingleVarConstraint(SingleAtomConstraint::new(
433            Variable::new("var"),
434            ThresholdConstraint::new(
435                ThresholdCompOp::Geq,
436                Vec::<(Parameter, Fraction)>::new(),
437                Fraction::new(2, 1, false),
438            ),
439        ));
440        let got = LIAVariableConstraint::try_from(expr).unwrap();
441        assert_eq!(got, expected_expr);
442    }
443
444    #[test]
445    fn test_try_into_multi_var() {
446        let expr = NonNegatedBooleanExpression::ComparisonExpression(
447            Box::new(
448                IntegerExpression::Atom(Variable::new("var1"))
449                    + IntegerExpression::Atom(Variable::new("var2")),
450            ),
451            ComparisonOp::Lt,
452            Box::new(IntegerExpression::Const(0)),
453        );
454
455        let expected_expr = LIAVariableConstraint::SumVarConstraint(
456            SumAtomConstraint::try_new(
457                BTreeMap::from([(Variable::new("var1"), 1), (Variable::new("var2"), 1)]),
458                ThresholdConstraint::new(
459                    ThresholdCompOp::Lt,
460                    Vec::<(Parameter, Fraction)>::new(),
461                    0,
462                ),
463            )
464            .unwrap(),
465        );
466
467        let got = LIAVariableConstraint::try_from(expr).unwrap();
468        assert_eq!(got, expected_expr);
469    }
470
471    #[test]
472    fn test_try_into_multi_var_double_var_lhs() {
473        let expr = NonNegatedBooleanExpression::ComparisonExpression(
474            Box::new(
475                IntegerExpression::Atom(Variable::new("var1"))
476                    + IntegerExpression::Atom(Variable::new("var2"))
477                    + IntegerExpression::Atom(Variable::new("var1")) * IntegerExpression::Const(2),
478            ),
479            ComparisonOp::Lt,
480            Box::new(IntegerExpression::Const(0)),
481        );
482
483        let expected_expr = LIAVariableConstraint::SumVarConstraint(
484            SumAtomConstraint::try_new(
485                BTreeMap::from([(Variable::new("var1"), 3), (Variable::new("var2"), 1)]),
486                ThresholdConstraint::new(
487                    ThresholdCompOp::Lt,
488                    Vec::<(Parameter, Fraction)>::new(),
489                    0,
490                ),
491            )
492            .unwrap(),
493        );
494
495        let got = LIAVariableConstraint::try_from(expr).unwrap();
496        assert_eq!(got, expected_expr);
497    }
498
499    #[test]
500    fn test_try_into_multi_var_double_var_rhs() {
501        // var1 + var2 + 5 * var1 <  0 + 2 * var1
502        let expr = NonNegatedBooleanExpression::ComparisonExpression(
503            Box::new(
504                IntegerExpression::Atom(Variable::new("var1"))
505                    + IntegerExpression::Atom(Variable::new("var2"))
506                    + IntegerExpression::Atom(Variable::new("var1")) * IntegerExpression::Const(5),
507            ),
508            ComparisonOp::Lt,
509            Box::new(
510                IntegerExpression::Const(0)
511                    + (IntegerExpression::Atom(Variable::new("var1"))
512                        * IntegerExpression::Const(2)),
513            ),
514        );
515
516        let expected_expr = LIAVariableConstraint::SumVarConstraint(
517            SumAtomConstraint::try_new(
518                BTreeMap::from([(Variable::new("var1"), 4), (Variable::new("var2"), 1)]),
519                ThresholdConstraint::new(
520                    ThresholdCompOp::Lt,
521                    Vec::<(Parameter, Fraction)>::new(),
522                    0,
523                ),
524            )
525            .unwrap(),
526        );
527
528        let got = LIAVariableConstraint::try_from(expr).unwrap();
529        assert_eq!(got, expected_expr);
530    }
531
532    #[test]
533    fn test_try_into_comp_guard() {
534        // var1 + var2 + 2 * var1 <  0 + 5 * var1
535        let expr = NonNegatedBooleanExpression::ComparisonExpression(
536            Box::new(
537                IntegerExpression::Atom(Variable::new("var1"))
538                    + IntegerExpression::Atom(Variable::new("var2"))
539                    + IntegerExpression::Atom(Variable::new("var1")) * IntegerExpression::Const(2),
540            ),
541            ComparisonOp::Lt,
542            Box::new(
543                IntegerExpression::Const(0)
544                    + (IntegerExpression::Atom(Variable::new("var1"))
545                        * IntegerExpression::Const(5)),
546            ),
547        );
548
549        let expected_expr = LIAVariableConstraint::ComparisonConstraint(
550            crate::lia_threshold_automaton::ComparisonConstraint::try_new(
551                BTreeMap::from([
552                    (Variable::new("var1"), -Fraction::from(2)),
553                    (Variable::new("var2"), 1.into()),
554                ]),
555                ThresholdConstraint::new(
556                    ThresholdCompOp::Lt,
557                    Vec::<(Parameter, Fraction)>::new(),
558                    0,
559                ),
560            )
561            .unwrap(),
562        );
563
564        let got = LIAVariableConstraint::try_from(expr).unwrap();
565        assert_eq!(got, expected_expr);
566    }
567
568    #[test]
569    fn test_try_into_only_const() {
570        // 0 == 0
571        let expr = NonNegatedBooleanExpression::ComparisonExpression(
572            Box::new(IntegerExpression::Const(0)),
573            ComparisonOp::Eq,
574            Box::new(IntegerExpression::Const(0)),
575        );
576
577        let got = LIAVariableConstraint::try_from(expr);
578
579        assert!(got.is_ok());
580        assert_eq!(
581            got.unwrap(),
582            LIAVariableConstraint::BinaryGuard(
583                Box::new(LIAVariableConstraint::True),
584                BooleanConnective::And,
585                Box::new(LIAVariableConstraint::True),
586            )
587        );
588    }
589
590    #[test]
591    fn test_error_on_parameter_constraint() {
592        let expr = NonNegatedBooleanExpression::ComparisonExpression(
593            Box::new(IntegerExpression::Param(Parameter::new("n"))),
594            ComparisonOp::Gt,
595            Box::new(IntegerExpression::Const(0)),
596        );
597
598        let got = LIAVariableConstraint::try_from(expr);
599        assert!(got.is_err(), "Got {got:?}");
600        assert!(matches!(
601            got,
602            Err(ConstraintRewriteError::ParameterConstraint(_))
603        ));
604    }
605
606    #[test]
607    fn test_try_into_const_comparisons_evaluated() {
608        let cases = [
609            (0, ComparisonOp::Lt, 0, LIAVariableConstraint::False),
610            (1, ComparisonOp::Lt, 2, LIAVariableConstraint::True),
611            (2, ComparisonOp::Lt, 1, LIAVariableConstraint::False),
612            (1, ComparisonOp::Leq, 2, LIAVariableConstraint::True),
613            (2, ComparisonOp::Gt, 1, LIAVariableConstraint::True),
614            (1, ComparisonOp::Gt, 2, LIAVariableConstraint::False),
615            (2, ComparisonOp::Geq, 1, LIAVariableConstraint::True),
616            (0, ComparisonOp::Neq, 1, LIAVariableConstraint::True),
617            (1, ComparisonOp::Eq, 2, LIAVariableConstraint::False),
618        ];
619
620        for (l, op, r, expected) in cases {
621            let expr = NonNegatedBooleanExpression::ComparisonExpression(
622                Box::new(IntegerExpression::Const(l)),
623                op,
624                Box::new(IntegerExpression::Const(r)),
625            );
626            let got = LIAVariableConstraint::try_from(expr).unwrap();
627            assert_eq!(got.is_top(), expected.is_top(), "{l} {op} {r}: got {got}");
628            assert_eq!(got.is_bot(), expected.is_bot(), "{l} {op} {r}: got {got}");
629        }
630    }
631}