1use crate::vocabulary::{VarId, Variable};
7use std::collections::BTreeMap;
8use std::hash::{Hash, Hasher};
9
10#[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 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 ((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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
88pub enum Constraint {
89 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 _ => 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#[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 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 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 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 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 pub fn is_satisfiable(&self) -> bool {
189 self.0.values().all(|c| !c.is_empty())
190 }
191
192 pub fn overlaps(&self, o: &Condition) -> bool {
194 self.and(o).is_satisfiable()
195 }
196
197 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 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 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 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 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}