Skip to main content

expectation_model/
condition.rs

1//! The condition algebra. A [`Condition`] is a conjunction of per-variable [`Constraint`]s and
2//! stands for the set of states that satisfy it. Conflict detection, the flow graph and story
3//! planning reduce to three questions about such sets: do two overlap, does one imply the
4//! other, and what is left after an action overwrites some variables.
5
6use crate::vocabulary::{VarId, Variable};
7use std::collections::BTreeMap;
8use std::hash::{Hash, Hasher};
9
10/// A numeric range with open or closed ends.
11#[derive(Clone, Copy, Debug)]
12pub struct Interval {
13    pub lo: f64,
14    pub lo_open: bool,
15    pub hi: f64,
16    pub hi_open: bool,
17}
18
19impl Interval {
20    pub fn closed(lo: f64, hi: f64) -> Self {
21        Interval { lo, lo_open: false, hi, hi_open: false }
22    }
23
24    pub fn point(v: f64) -> Self {
25        Self::closed(v, v)
26    }
27
28    pub fn is_empty(&self) -> bool {
29        self.lo > self.hi || (self.lo == self.hi && (self.lo_open || self.hi_open))
30    }
31
32    pub fn contains(&self, v: f64) -> bool {
33        let above = if self.lo_open { v > self.lo } else { v >= self.lo };
34        let below = if self.hi_open { v < self.hi } else { v <= self.hi };
35        above && below
36    }
37
38    pub fn midpoint(&self) -> f64 {
39        0.5 * (self.lo + self.hi)
40    }
41
42    pub fn intersect(&self, o: &Interval) -> Interval {
43        let (lo, lo_open) = match self.lo.partial_cmp(&o.lo) {
44            Some(std::cmp::Ordering::Greater) => (self.lo, self.lo_open),
45            Some(std::cmp::Ordering::Less) => (o.lo, o.lo_open),
46            _ => (self.lo, self.lo_open || o.lo_open),
47        };
48        let (hi, hi_open) = match self.hi.partial_cmp(&o.hi) {
49            Some(std::cmp::Ordering::Less) => (self.hi, self.hi_open),
50            Some(std::cmp::Ordering::Greater) => (o.hi, o.hi_open),
51            _ => (self.hi, self.hi_open || o.hi_open),
52        };
53        Interval { lo, lo_open, hi, hi_open }
54    }
55
56    /// Is every value in `self` also in `o`?
57    pub fn within(&self, o: &Interval) -> bool {
58        if self.is_empty() {
59            return true;
60        }
61        let lo = o.lo < self.lo || (o.lo == self.lo && (!o.lo_open || self.lo_open));
62        let hi = o.hi > self.hi || (o.hi == self.hi && (!o.hi_open || self.hi_open));
63        lo && hi
64    }
65
66    fn bits(&self) -> (u64, bool, u64, bool) {
67        // +0.0 and -0.0 are the same bound.
68        ((self.lo + 0.0).to_bits(), self.lo_open, (self.hi + 0.0).to_bits(), self.hi_open)
69    }
70}
71
72impl PartialEq for Interval {
73    fn eq(&self, o: &Self) -> bool {
74        self.bits() == o.bits()
75    }
76}
77
78impl Eq for Interval {}
79
80impl Hash for Interval {
81    fn hash<H: Hasher>(&self, h: &mut H) {
82        self.bits().hash(h)
83    }
84}
85
86/// The values one variable may take.
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
88pub enum Constraint {
89    /// Bit `i` set = the variable's `i`th declared value is allowed.
90    OneOf(u64),
91    Range(Interval),
92}
93
94impl Constraint {
95    pub fn is_empty(&self) -> bool {
96        match self {
97            Constraint::OneOf(m) => *m == 0,
98            Constraint::Range(r) => r.is_empty(),
99        }
100    }
101
102    pub fn intersect(&self, o: &Constraint) -> Constraint {
103        match (self, o) {
104            (Constraint::OneOf(a), Constraint::OneOf(b)) => Constraint::OneOf(a & b),
105            (Constraint::Range(a), Constraint::Range(b)) => Constraint::Range(a.intersect(b)),
106            // A variable has one kind; mixing them is a bug upstream, and nothing satisfies both.
107            _ => Constraint::OneOf(0),
108        }
109    }
110
111    pub fn within(&self, o: &Constraint) -> bool {
112        match (self, o) {
113            (Constraint::OneOf(a), Constraint::OneOf(b)) => a & !b == 0,
114            (Constraint::Range(a), Constraint::Range(b)) => a.within(b),
115            _ => self.is_empty(),
116        }
117    }
118
119    pub fn overlaps(&self, o: &Constraint) -> bool {
120        !self.intersect(o).is_empty()
121    }
122}
123
124/// A set of states, written as a conjunction of constraints. Variables it doesn't mention are
125/// unconstrained.
126///
127/// Invariant: a stored constraint is never a variable's full domain, so "mentioned" always
128/// means "narrowed". [`Condition::constrain`] keeps that true; the other operations preserve it.
129#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
130pub struct Condition(BTreeMap<VarId, Constraint>);
131
132impl Condition {
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// No constraints at all: every state.
138    pub fn is_unconstrained(&self) -> bool {
139        self.0.is_empty()
140    }
141
142    pub fn get(&self, v: VarId) -> Option<&Constraint> {
143        self.0.get(&v)
144    }
145
146    pub fn iter(&self) -> impl Iterator<Item = (VarId, &Constraint)> + '_ {
147        self.0.iter().map(|(v, c)| (*v, c))
148    }
149
150    pub fn vars(&self) -> impl Iterator<Item = VarId> + '_ {
151        self.0.keys().copied()
152    }
153
154    /// Narrow `var` to `c` (on top of anything already required of it).
155    pub fn constrain(&mut self, var: &Variable, c: Constraint) {
156        let c = c.intersect(&var.full());
157        let c = match self.0.get(&var.id) {
158            Some(prev) => prev.intersect(&c),
159            None => c,
160        };
161        if c == var.full() {
162            self.0.remove(&var.id);
163        } else {
164            self.0.insert(var.id, c);
165        }
166    }
167
168    /// Replace whatever is required of `var` with `c`.
169    pub fn set(&mut self, var: &Variable, c: Constraint) {
170        let c = c.intersect(&var.full());
171        if c == var.full() {
172            self.0.remove(&var.id);
173        } else {
174            self.0.insert(var.id, c);
175        }
176    }
177
178    /// States in both.
179    pub fn and(&self, o: &Condition) -> Condition {
180        let mut out = self.0.clone();
181        for (v, c) in &o.0 {
182            out.entry(*v).and_modify(|e| *e = e.intersect(c)).or_insert(*c);
183        }
184        Condition(out)
185    }
186
187    /// Does any state satisfy it?
188    pub fn is_satisfiable(&self) -> bool {
189        self.0.values().all(|c| !c.is_empty())
190    }
191
192    /// Is there a state in both?
193    pub fn overlaps(&self, o: &Condition) -> bool {
194        self.and(o).is_satisfiable()
195    }
196
197    /// Is every state of `self` also in `o`?
198    pub fn implies(&self, o: &Condition) -> bool {
199        !self.is_satisfiable() || o.0.iter().all(|(v, c)| self.0.get(v).is_some_and(|s| s.within(c)))
200    }
201
202    /// `self` with `o`'s variables replaced by what `o` says: the state after an action that
203    /// sets those variables, leaving the rest alone.
204    pub fn overlay(&self, o: &Condition) -> Condition {
205        let mut out = self.0.clone();
206        out.extend(o.0.iter().map(|(v, c)| (*v, *c)));
207        Condition(out)
208    }
209
210    /// Variables both mention but on which they can't both hold.
211    pub fn contradictions(&self, o: &Condition) -> Vec<VarId> {
212        self.0.iter().filter(|(v, c)| o.0.get(v).is_some_and(|d| !c.overlaps(d))).map(|(v, _)| *v).collect()
213    }
214
215    /// Only the given variables.
216    pub fn restrict(&self, vars: impl IntoIterator<Item = VarId>) -> Condition {
217        Condition(vars.into_iter().filter_map(|v| self.0.get(&v).map(|c| (v, *c))).collect())
218    }
219
220    /// The parts of `o` that `self` doesn't already guarantee.
221    pub fn unproven(&self, o: &Condition) -> Condition {
222        Condition(
223            o.0.iter().filter(|(v, c)| !self.0.get(v).is_some_and(|s| s.within(c))).map(|(v, c)| (*v, *c)).collect(),
224        )
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::diagnostic::Span;
232    use crate::vocabulary::VarKind;
233
234    fn var(id: u32, kind: VarKind) -> Variable {
235        Variable {
236            id: VarId(id),
237            subject: format!("v{id}"),
238            key: format!("v{id}"),
239            kind,
240            verb: "is".into(),
241            set: None,
242            shows: vec![],
243            span: Span::new("t", 1),
244        }
245    }
246
247    fn above(v: f64) -> Constraint {
248        Constraint::Range(Interval { lo: v, lo_open: true, hi: 100.0, hi_open: false })
249    }
250
251    #[test]
252    fn intervals() {
253        let a = Interval { lo: 55.0, lo_open: true, hi: 100.0, hi_open: false };
254        let b = Interval { lo: 0.0, lo_open: false, hi: 55.0, hi_open: false };
255        assert!(a.intersect(&b).is_empty());
256        assert!(!a.intersect(&Interval::closed(50.0, 60.0)).is_empty());
257        assert!(Interval::point(80.0).within(&a));
258        assert!(!Interval::point(55.0).within(&a));
259        assert!(a.within(&Interval::closed(0.0, 100.0)));
260        assert_eq!(Interval::point(0.0), Interval::point(-0.0));
261    }
262
263    #[test]
264    fn full_domains_are_not_stored() {
265        let thr = var(0, VarKind::Number { min: 0.0, max: 100.0, unit: None });
266        let mut c = Condition::new();
267        c.constrain(&thr, Constraint::Range(Interval::closed(-10.0, 200.0)));
268        assert!(c.is_unconstrained());
269        c.constrain(&thr, above(55.0));
270        assert_eq!(c.vars().count(), 1);
271    }
272
273    #[test]
274    fn implication_and_overlay() {
275        let phase = var(0, VarKind::Enum(vec!["off".into(), "heating".into(), "dry".into()]));
276        let thr = var(1, VarKind::Number { min: 0.0, max: 100.0, unit: None });
277        let mut held = Condition::new();
278        held.constrain(&phase, Constraint::OneOf(0b001));
279        let mut held_hot = held.clone();
280        held_hot.constrain(&thr, above(55.0));
281        assert!(held_hot.implies(&held));
282        assert!(!held.implies(&held_hot));
283        assert!(held.overlaps(&held_hot));
284
285        let mut heating = Condition::new();
286        heating.constrain(&phase, Constraint::OneOf(0b010));
287        assert!(!held.overlaps(&heating));
288        assert_eq!(held.contradictions(&heating), vec![VarId(0)]);
289        let after = held_hot.overlay(&heating);
290        assert!(after.implies(&heating));
291        assert_eq!(after.get(VarId(1)), Some(&above(55.0)));
292    }
293}