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