Skip to main content

pumpkin_propagators/propagators/
element.rs

1//! Contains the propagator for the [Element](https://sofdem.github.io/gccat/gccat/Celement.html)
2//! constraint.
3#![allow(clippy::double_parens, reason = "originates inside the bitfield macro")]
4
5use std::cell::RefCell;
6
7use bitfield_struct::bitfield;
8use pumpkin_checking::AtomicConstraint;
9use pumpkin_checking::CheckerVariable;
10use pumpkin_checking::Domain;
11use pumpkin_checking::InferenceChecker;
12use pumpkin_checking::Union;
13use pumpkin_core::conjunction;
14use pumpkin_core::declare_inference_label;
15use pumpkin_core::predicate;
16use pumpkin_core::predicates::Predicate;
17use pumpkin_core::proof::ConstraintTag;
18use pumpkin_core::proof::InferenceCode;
19use pumpkin_core::propagation::DomainEvents;
20use pumpkin_core::propagation::EventsToRegister;
21use pumpkin_core::propagation::ExplanationContext;
22use pumpkin_core::propagation::LazyExplanation;
23use pumpkin_core::propagation::LocalId;
24use pumpkin_core::propagation::Priority;
25use pumpkin_core::propagation::PropagationContext;
26use pumpkin_core::propagation::Propagator;
27use pumpkin_core::propagation::PropagatorConstructor;
28use pumpkin_core::propagation::PropagatorConstructorContext;
29use pumpkin_core::propagation::PropagatorSpec;
30use pumpkin_core::propagation::ReadDomains;
31use pumpkin_core::propagation::RuntimeCheckers;
32use pumpkin_core::state::PropagationStatusCP;
33use pumpkin_core::variables::IntegerVariable;
34use pumpkin_core::variables::Reason;
35
36#[derive(Clone, Debug)]
37pub struct ElementArgs<VX, VI, VE> {
38    pub array: Box<[VX]>,
39    pub index: VI,
40    pub rhs: VE,
41    pub constraint_tag: ConstraintTag,
42}
43
44declare_inference_label!(Element);
45
46impl<VX, VI, VE> PropagatorConstructor for ElementArgs<VX, VI, VE>
47where
48    VX: IntegerVariable + 'static,
49    VI: IntegerVariable + 'static,
50    VE: IntegerVariable + 'static,
51{
52    type PropagatorImpl = ElementPropagator<VX, VI, VE>;
53
54    fn create(self, _: PropagatorConstructorContext) -> PropagatorSpec<Self::PropagatorImpl> {
55        let ElementArgs {
56            array,
57            index,
58            rhs,
59            constraint_tag,
60        } = self;
61
62        let mut registration = EventsToRegister::builder();
63        for (i, x_i) in array.iter().enumerate() {
64            registration = registration.add(
65                x_i,
66                DomainEvents::ANY_INT,
67                LocalId::from(i as u32 + ID_X_OFFSET),
68            );
69        }
70
71        registration = registration.add(&index, DomainEvents::ANY_INT, ID_INDEX);
72        registration = registration.add(&rhs, DomainEvents::ANY_INT, ID_RHS);
73
74        let mut checkers = RuntimeCheckers::builder();
75        let inference_code = checkers.add_inference_checker(
76            constraint_tag,
77            Element,
78            ElementChecker::new(array.clone(), index.clone(), rhs.clone()),
79        );
80
81        let propagator = ElementPropagator {
82            array,
83            index,
84            rhs,
85            inference_code,
86            rhs_reason_buffer: vec![],
87        };
88
89        PropagatorSpec {
90            registration: registration.build(),
91            checkers: checkers.build(),
92            propagator,
93        }
94    }
95}
96
97const ID_INDEX: LocalId = LocalId::from(0);
98const ID_RHS: LocalId = LocalId::from(1);
99
100// local ids of array vars are shifted by ID_X_OFFSET
101const ID_X_OFFSET: u32 = 2;
102
103/// Arc-consistent propagator for constraint `element([x_1, \ldots, x_n], i, e)`, where `x_j` are
104///  variables, `i` is an integer variable, and `e` is a variable, which holds iff `x_i = e`
105///
106/// Note that this propagator is 0-indexed
107#[derive(Clone, Debug)]
108pub struct ElementPropagator<VX, VI, VE> {
109    array: Box<[VX]>,
110    index: VI,
111    rhs: VE,
112    inference_code: InferenceCode,
113
114    rhs_reason_buffer: Vec<Predicate>,
115}
116
117impl<VX, VI, VE> Propagator for ElementPropagator<VX, VI, VE>
118where
119    VX: IntegerVariable + 'static,
120    VI: IntegerVariable + 'static,
121    VE: IntegerVariable + 'static,
122{
123    fn priority(&self) -> Priority {
124        Priority::Low
125    }
126
127    fn name(&self) -> &str {
128        "Element"
129    }
130
131    fn propagate_from_scratch(&self, mut context: PropagationContext) -> PropagationStatusCP {
132        self.propagate_index_bounds_within_array(&mut context)?;
133
134        self.propagate_rhs_bounds_based_on_array(&mut context)?;
135
136        self.propagate_index_based_on_domain_intersection_with_rhs(&mut context)?;
137
138        if let Some(idx) = context.fixed_value(&self.index) {
139            self.propagate_equality(&mut context, idx)?;
140        }
141
142        Ok(())
143    }
144
145    fn lazy_explanation(&mut self, code: u64, context: ExplanationContext) -> LazyExplanation<'_> {
146        let payload = RightHandSideReason::from_bits(code);
147
148        self.rhs_reason_buffer.clear();
149        self.rhs_reason_buffer
150            .extend(self.array.iter().enumerate().map(|(idx, variable)| {
151                if context.contains_at_trail_position(
152                    &self.index,
153                    idx as i32,
154                    context.get_trail_position(),
155                ) {
156                    match payload.bound() {
157                        Bound::Lower => predicate![variable >= payload.value()],
158                        Bound::Upper => predicate![variable <= payload.value()],
159                    }
160                } else {
161                    predicate![self.index != idx as i32]
162                }
163            }));
164
165        LazyExplanation {
166            predicates: self.rhs_reason_buffer.as_slice(),
167            inference_code: self.inference_code.clone(),
168        }
169    }
170}
171
172impl<VX, VI, VE> ElementPropagator<VX, VI, VE>
173where
174    VX: IntegerVariable + 'static,
175    VI: IntegerVariable + 'static,
176    VE: IntegerVariable + 'static,
177{
178    /// Propagate the bounds of `self.index` to be in the range `[0, self.array.len())`.
179    fn propagate_index_bounds_within_array(
180        &self,
181        context: &mut PropagationContext<'_>,
182    ) -> PropagationStatusCP {
183        context.post(
184            predicate![self.index >= 0],
185            (conjunction!(), &self.inference_code),
186        )?;
187        context.post(
188            predicate![self.index <= self.array.len() as i32 - 1],
189            (conjunction!(), &self.inference_code),
190        )?;
191        Ok(())
192    }
193
194    /// The lower bound (resp. upper bound) of the right-hand side can be the minimum lower
195    /// bound (res. maximum upper bound) of the elements.
196    fn propagate_rhs_bounds_based_on_array(
197        &self,
198        context: &mut PropagationContext<'_>,
199    ) -> PropagationStatusCP {
200        let (rhs_lb, rhs_ub) = self
201            .array
202            .iter()
203            .enumerate()
204            .filter(|(idx, _)| context.contains(&self.index, *idx as i32))
205            .fold((i32::MAX, i32::MIN), |(rhs_lb, rhs_ub), (_, element)| {
206                (
207                    i32::min(rhs_lb, context.lower_bound(element)),
208                    i32::max(rhs_ub, context.upper_bound(element)),
209                )
210            });
211
212        context.post(
213            predicate![self.rhs >= rhs_lb],
214            Reason::DynamicLazy(
215                RightHandSideReason::new()
216                    .with_bound(Bound::Lower)
217                    .with_value(rhs_lb)
218                    .into_bits(),
219            ),
220        )?;
221        context.post(
222            predicate![self.rhs <= rhs_ub],
223            Reason::DynamicLazy(
224                RightHandSideReason::new()
225                    .with_bound(Bound::Upper)
226                    .with_value(rhs_ub)
227                    .into_bits(),
228            ),
229        )?;
230
231        Ok(())
232    }
233
234    /// Go through the array. For every element for which the domain does not intersect with the
235    /// right-hand side, remove it from index.
236    fn propagate_index_based_on_domain_intersection_with_rhs(
237        &self,
238        context: &mut PropagationContext<'_>,
239    ) -> PropagationStatusCP {
240        let rhs_lb = context.lower_bound(&self.rhs);
241        let rhs_ub = context.upper_bound(&self.rhs);
242        let mut to_remove = vec![];
243        for idx in context.iterate_domain(&self.index) {
244            let element = &self.array[idx as usize];
245
246            let element_ub = context.upper_bound(element);
247            let element_lb = context.lower_bound(element);
248
249            let reason = if rhs_lb > element_ub {
250                conjunction!([element <= rhs_lb - 1] & [self.rhs >= rhs_lb])
251            } else if rhs_ub < element_lb {
252                conjunction!([element >= rhs_ub + 1] & [self.rhs <= rhs_ub])
253            } else {
254                continue;
255            };
256
257            to_remove.push((idx, reason));
258        }
259
260        for (idx, reason) in to_remove.drain(..) {
261            context.post(
262                predicate![self.index != idx],
263                (reason, &self.inference_code),
264            )?;
265        }
266
267        Ok(())
268    }
269
270    /// Propagate equality between lhs and rhs. This assumes the bounds of rhs have already been
271    /// tightened to the bounds of lhs, through a previous propagation rule.
272    fn propagate_equality(
273        &self,
274        context: &mut PropagationContext<'_>,
275        index: i32,
276    ) -> PropagationStatusCP {
277        let rhs_lb = context.lower_bound(&self.rhs);
278        let rhs_ub = context.upper_bound(&self.rhs);
279        let lhs = &self.array[index as usize];
280
281        context.post(
282            predicate![lhs >= rhs_lb],
283            (
284                conjunction!([self.rhs >= rhs_lb] & [self.index == index]),
285                &self.inference_code,
286            ),
287        )?;
288        context.post(
289            predicate![lhs <= rhs_ub],
290            (
291                conjunction!([self.rhs <= rhs_ub] & [self.index == index]),
292                &self.inference_code,
293            ),
294        )?;
295        Ok(())
296    }
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq)]
300#[repr(u8)]
301enum Bound {
302    Lower = 0,
303    Upper = 1,
304}
305
306impl Bound {
307    const fn into_bits(self) -> u8 {
308        self as _
309    }
310
311    const fn from_bits(value: u8) -> Self {
312        match value {
313            0 => Bound::Lower,
314            _ => Bound::Upper,
315        }
316    }
317}
318
319#[bitfield(u64)]
320struct RightHandSideReason {
321    #[bits(32, from = Bound::from_bits)]
322    bound: Bound,
323    value: i32,
324}
325
326#[derive(Clone, Debug)]
327pub struct ElementChecker<VX, VI, VE> {
328    array: Box<[VX]>,
329    index: VI,
330    rhs: VE,
331
332    union: RefCell<Union>,
333}
334
335impl<VX, VI, VE> ElementChecker<VX, VI, VE> {
336    /// Create a new [`ElementChecker`].
337    pub fn new(array: Box<[VX]>, index: VI, rhs: VE) -> Self {
338        ElementChecker {
339            array,
340            index,
341            rhs,
342            union: RefCell::new(Union::empty()),
343        }
344    }
345}
346
347impl<VX, VI, VE, Atomic> InferenceChecker<Atomic> for ElementChecker<VX, VI, VE>
348where
349    Atomic: AtomicConstraint,
350    VX: CheckerVariable<Atomic>,
351    VI: CheckerVariable<Atomic>,
352    VE: CheckerVariable<Atomic>,
353{
354    fn check(
355        &self,
356        state: pumpkin_checking::VariableState<Atomic>,
357        _: &[Atomic],
358        _: Option<&Atomic>,
359    ) -> bool {
360        self.union.borrow_mut().reset();
361
362        // A domain consistent checker for element does the following:
363        // 1. Determine the elements in the array whose index is in the domain of the index
364        //    variable.
365        // 2. Take the union of the domains of those elements.
366        // 3. Intersect that union with the domain on the right-hand side.
367        //
368        // The intersection should be empty for a conflict to exist.
369        let supported_elements: Vec<_> = self
370            .array
371            .iter()
372            .enumerate()
373            .filter(|(idx, _)| self.index.induced_domain_contains(&state, *idx as i32))
374            .map(|(_, element)| element)
375            .collect();
376
377        for element in supported_elements {
378            self.union.borrow_mut().add(&state, element);
379        }
380
381        assert!(
382            self.union.borrow().is_consistent(),
383            "at least one element has a non-empty domain or else variable state would be inconsistent"
384        );
385
386        // Compute `|union cap rhs| == 0`.
387        let intersection_lower_bound = self
388            .union
389            .borrow()
390            .lower_bound()
391            .max(self.rhs.induced_lower_bound(&state));
392        let intersection_upper_bound = self
393            .union
394            .borrow()
395            .upper_bound()
396            .min(self.rhs.induced_upper_bound(&state));
397        let holes = self
398            .union
399            .borrow()
400            .holes()
401            .chain(self.rhs.induced_holes(&state))
402            .collect();
403
404        let intersected_domain =
405            Domain::new(intersection_lower_bound, intersection_upper_bound, holes);
406
407        !intersected_domain.is_consistent()
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use pumpkin_checking::TestAtomic;
414    use pumpkin_checking::VariableState;
415    use pumpkin_core::predicate;
416    use pumpkin_core::predicates::Predicate;
417    use pumpkin_core::predicates::PropositionalConjunction;
418    use pumpkin_core::propagation::CurrentNogood;
419    use pumpkin_core::state::State;
420
421    use super::*;
422    use crate::StateExt;
423
424    #[test]
425    fn elements_from_array_with_disjoint_domains_to_rhs_are_filtered_from_index() {
426        let mut state = State::default();
427
428        let x_0 = state.new_interval_variable(4, 6, None);
429        let x_1 = state.new_interval_variable(2, 3, None);
430        let x_2 = state.new_interval_variable(7, 9, None);
431        let x_3 = state.new_interval_variable(14, 15, None);
432
433        let index = state.new_interval_variable(0, 3, None);
434        let rhs = state.new_interval_variable(6, 9, None);
435        let constraint_tag = state.new_constraint_tag();
436
437        let _ = state.add_propagator(ElementArgs {
438            array: vec![x_0, x_1, x_2, x_3].into(),
439            index,
440            rhs,
441            constraint_tag,
442        });
443        state.propagate_to_fixed_point().expect("no empty domains");
444
445        state.assert_bounds(index, 0, 2);
446
447        let mut reason_buffer: Vec<Predicate> = vec![];
448        let _ = state.get_propagation_reason(
449            predicate![index != 3],
450            &mut reason_buffer,
451            CurrentNogood::empty(),
452        );
453        let reason: PropositionalConjunction = reason_buffer.into();
454        assert_eq!(conjunction!([x_3 >= 10] & [rhs <= 9]), reason);
455
456        let mut reason_buffer: Vec<Predicate> = vec![];
457        let _ = state.get_propagation_reason(
458            predicate![index != 1],
459            &mut reason_buffer,
460            CurrentNogood::empty(),
461        );
462        let reason: PropositionalConjunction = reason_buffer.into();
463        assert_eq!(conjunction!([x_1 <= 5] & [rhs >= 6]), reason);
464    }
465
466    #[test]
467    fn bounds_of_rhs_are_min_and_max_of_lower_and_upper_in_array() {
468        let mut state = State::default();
469
470        let x_0 = state.new_interval_variable(3, 10, None);
471        let x_1 = state.new_interval_variable(2, 3, None);
472        let x_2 = state.new_interval_variable(7, 9, None);
473        let x_3 = state.new_interval_variable(14, 15, None);
474
475        let index = state.new_interval_variable(0, 3, None);
476        let rhs = state.new_interval_variable(0, 20, None);
477        let constraint_tag = state.new_constraint_tag();
478
479        let _ = state.add_propagator(ElementArgs {
480            array: vec![x_0, x_1, x_2, x_3].into(),
481            index,
482            rhs,
483            constraint_tag,
484        });
485        state.propagate_to_fixed_point().expect("no empty domains");
486
487        state.assert_bounds(rhs, 2, 15);
488
489        let mut reason_buffer: Vec<Predicate> = vec![];
490        let _ = state.get_propagation_reason(
491            predicate![rhs >= 2],
492            &mut reason_buffer,
493            CurrentNogood::empty(),
494        );
495        let reason: PropositionalConjunction = reason_buffer.into();
496        assert_eq!(
497            conjunction!([x_0 >= 2] & [x_1 >= 2] & [x_2 >= 2] & [x_3 >= 2]),
498            reason
499        );
500
501        let mut reason_buffer: Vec<Predicate> = vec![];
502        let _ = state.get_propagation_reason(
503            predicate![rhs <= 15],
504            &mut reason_buffer,
505            CurrentNogood::empty(),
506        );
507        let reason: PropositionalConjunction = reason_buffer.into();
508        assert_eq!(
509            conjunction!([x_0 <= 15] & [x_1 <= 15] & [x_2 <= 15] & [x_3 <= 15]),
510            reason
511        );
512    }
513
514    #[test]
515    fn fixed_index_propagates_bounds_on_element() {
516        let mut state = State::default();
517
518        let x_0 = state.new_interval_variable(3, 10, None);
519        let x_1 = state.new_interval_variable(0, 15, None);
520        let x_2 = state.new_interval_variable(7, 9, None);
521        let x_3 = state.new_interval_variable(14, 15, None);
522        let constraint_tag = state.new_constraint_tag();
523
524        let index = state.new_interval_variable(1, 1, None);
525        let rhs = state.new_interval_variable(6, 9, None);
526
527        let _ = state.add_propagator(ElementArgs {
528            array: vec![x_0, x_1, x_2, x_3].into(),
529            index,
530            rhs,
531            constraint_tag,
532        });
533        state.propagate_to_fixed_point().expect("no empty domains");
534
535        state.assert_bounds(x_1, 6, 9);
536
537        let mut reason_buffer: Vec<Predicate> = vec![];
538        let _ = state.get_propagation_reason(
539            predicate![x_1 >= 6],
540            &mut reason_buffer,
541            CurrentNogood::empty(),
542        );
543        let reason: PropositionalConjunction = reason_buffer.into();
544        assert_eq!(conjunction!([index == 1] & [rhs >= 6]), reason);
545
546        let mut reason_buffer: Vec<Predicate> = vec![];
547        let _ = state.get_propagation_reason(
548            predicate![x_1 <= 9],
549            &mut reason_buffer,
550            CurrentNogood::empty(),
551        );
552        let reason: PropositionalConjunction = reason_buffer.into();
553        assert_eq!(conjunction!([index == 1] & [rhs <= 9]), reason);
554    }
555
556    #[test]
557    fn index_hole_propagates_bounds_on_rhs() {
558        let mut state = State::default();
559
560        let x_0 = state.new_interval_variable(3, 10, None);
561        let x_1 = state.new_interval_variable(0, 15, None);
562        let x_2 = state.new_interval_variable(7, 9, None);
563        let x_3 = state.new_interval_variable(14, 15, None);
564        let constraint_tag = state.new_constraint_tag();
565
566        let index = state.new_interval_variable(0, 3, None);
567        let _ = state
568            .post(predicate![index != 1])
569            .expect("Value can be removed");
570
571        let rhs = state.new_interval_variable(-10, 30, None);
572
573        let _ = state.add_propagator(ElementArgs {
574            array: vec![x_0, x_1, x_2, x_3].into(),
575            index,
576            rhs,
577            constraint_tag,
578        });
579        state.propagate_to_fixed_point().expect("no empty domains");
580
581        state.assert_bounds(rhs, 3, 15);
582
583        let mut reason_buffer: Vec<Predicate> = vec![];
584        let _ = state.get_propagation_reason(
585            predicate![rhs >= 3],
586            &mut reason_buffer,
587            CurrentNogood::empty(),
588        );
589        let reason: PropositionalConjunction = reason_buffer.into();
590        assert_eq!(
591            conjunction!([x_0 >= 3] & [x_2 >= 3] & [x_3 >= 3] & [index != 1]),
592            reason
593        );
594
595        let mut reason_buffer: Vec<Predicate> = vec![];
596        let _ = state.get_propagation_reason(
597            predicate![rhs <= 15],
598            &mut reason_buffer,
599            CurrentNogood::empty(),
600        );
601        let reason: PropositionalConjunction = reason_buffer.into();
602        assert_eq!(
603            conjunction!([x_0 <= 15] & [x_2 <= 15] & [x_3 <= 15] & [index != 1]),
604            reason
605        );
606    }
607
608    #[test]
609    fn holes_outside_union_bounds_are_ignored() {
610        let premises = [
611            TestAtomic {
612                name: "x1",
613                comparison: pumpkin_checking::Comparison::GreaterEqual,
614                value: 4,
615            },
616            TestAtomic {
617                name: "x2",
618                comparison: pumpkin_checking::Comparison::NotEqual,
619                value: 2,
620            },
621        ];
622
623        let consequent = Some(TestAtomic {
624            name: "x4",
625            comparison: pumpkin_checking::Comparison::NotEqual,
626            value: 2,
627        });
628        let state = VariableState::prepare_for_conflict_check(premises, consequent)
629            .expect("no conflicting atomics");
630
631        let checker = ElementChecker::new(vec!["x1", "x2"].into(), "x3", "x4");
632
633        assert!(checker.check(state, &premises, consequent.as_ref()));
634    }
635}