Skip to main content

pumpkin_propagators/propagators/arithmetic/
maximum.rs

1use pumpkin_checking::AtomicConstraint;
2use pumpkin_checking::CheckerVariable;
3use pumpkin_checking::InferenceChecker;
4use pumpkin_checking::IntExt;
5use pumpkin_core::conjunction;
6use pumpkin_core::declare_inference_label;
7use pumpkin_core::predicate;
8use pumpkin_core::predicates::PropositionalConjunction;
9use pumpkin_core::proof::ConstraintTag;
10use pumpkin_core::proof::InferenceCode;
11use pumpkin_core::propagation::DomainEvents;
12use pumpkin_core::propagation::InferenceCheckers;
13use pumpkin_core::propagation::LocalId;
14use pumpkin_core::propagation::Priority;
15use pumpkin_core::propagation::PropagationContext;
16use pumpkin_core::propagation::Propagator;
17use pumpkin_core::propagation::PropagatorConstructor;
18use pumpkin_core::propagation::PropagatorConstructorContext;
19use pumpkin_core::propagation::ReadDomains;
20use pumpkin_core::state::PropagationStatusCP;
21use pumpkin_core::variables::IntegerVariable;
22
23#[derive(Clone, Debug)]
24pub struct MaximumArgs<ElementVar, Rhs> {
25    pub array: Box<[ElementVar]>,
26    pub rhs: Rhs,
27    pub constraint_tag: ConstraintTag,
28}
29
30declare_inference_label!(Maximum);
31
32impl<ElementVar, Rhs> PropagatorConstructor for MaximumArgs<ElementVar, Rhs>
33where
34    ElementVar: IntegerVariable + 'static,
35    Rhs: IntegerVariable + 'static,
36{
37    type PropagatorImpl = MaximumPropagator<ElementVar, Rhs>;
38
39    fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
40        checkers.add_inference_checker(
41            InferenceCode::new(self.constraint_tag, Maximum),
42            Box::new(MaximumChecker {
43                array: self.array.clone(),
44                rhs: self.rhs.clone(),
45            }),
46        );
47    }
48
49    fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
50        let MaximumArgs {
51            array,
52            rhs,
53            constraint_tag,
54        } = self;
55
56        for (idx, var) in array.iter().enumerate() {
57            context.register(var.clone(), DomainEvents::BOUNDS, LocalId::from(idx as u32));
58        }
59
60        context.register(
61            rhs.clone(),
62            DomainEvents::BOUNDS,
63            LocalId::from(array.len() as u32),
64        );
65
66        let inference_code = InferenceCode::new(constraint_tag, Maximum);
67
68        MaximumPropagator {
69            array,
70            rhs,
71            inference_code,
72        }
73    }
74}
75
76/// Bounds-consistent propagator which enforces `max(array) = rhs`. Can be constructed through
77/// [`MaximumArgs`].
78#[derive(Clone, Debug)]
79pub struct MaximumPropagator<ElementVar, Rhs> {
80    array: Box<[ElementVar]>,
81    rhs: Rhs,
82    inference_code: InferenceCode,
83}
84
85impl<ElementVar: IntegerVariable + 'static, Rhs: IntegerVariable + 'static> Propagator
86    for MaximumPropagator<ElementVar, Rhs>
87{
88    fn priority(&self) -> Priority {
89        Priority::High
90    }
91
92    fn name(&self) -> &str {
93        "Maximum"
94    }
95
96    fn propagate_from_scratch(&self, mut context: PropagationContext) -> PropagationStatusCP {
97        // This is the constraint that is being propagated:
98        // max(a_0, a_1, ..., a_{n-1}) = rhs
99
100        let rhs_ub = context.upper_bound(&self.rhs);
101        let mut max_ub = context.upper_bound(&self.array[0]);
102        let mut max_lb = context.lower_bound(&self.array[0]);
103        let mut lb_reason = predicate![self.array[0] >= max_lb];
104        for var in self.array.iter() {
105            // Rule 1.
106            // UB(a_i) <= UB(rhs, constraint_tag }
107            context.post(
108                predicate![var <= rhs_ub],
109                (conjunction!([self.rhs <= rhs_ub]), &self.inference_code),
110            )?;
111
112            let var_lb = context.lower_bound(var);
113            let var_ub = context.upper_bound(var);
114
115            if var_lb > max_lb {
116                max_lb = var_lb;
117                lb_reason = predicate![var >= var_lb];
118            }
119
120            if var_ub > max_ub {
121                max_ub = var_ub;
122            }
123        }
124        // Rule 2.
125        // LB(rhs, constraint_tag } >= max{LB(a_i)}.
126        context.post(
127            predicate![self.rhs >= max_lb],
128            (
129                PropositionalConjunction::from(lb_reason),
130                &self.inference_code,
131            ),
132        )?;
133
134        // Rule 3.
135        // UB(rhs, constraint_tag } <= max{UB(a_i)}.
136        // Note that this implicitly also covers the rule:
137        // 'if LB(rhs, constraint_tag } > UB(a_i) for all i, then conflict'.
138        if rhs_ub > max_ub {
139            let ub_reason: PropositionalConjunction = self
140                .array
141                .iter()
142                .map(|var| predicate![var <= max_ub])
143                .collect();
144            context.post(
145                predicate![self.rhs <= max_ub],
146                (ub_reason, &self.inference_code),
147            )?;
148        }
149
150        // Rule 4.
151        // If there is only one variable with UB(a_i) >= LB(rhs, constraint_tag },
152        // then the bounds for rhs and that variable should be intersected.
153        let rhs_lb = context.lower_bound(&self.rhs);
154        let mut propagating_variable: Option<&ElementVar> = None;
155        let mut propagation_reason = PropositionalConjunction::default();
156        for var in self.array.iter() {
157            if context.upper_bound(var) >= rhs_lb {
158                if propagating_variable.is_none() {
159                    propagating_variable = Some(var);
160                } else {
161                    propagating_variable = None;
162                    break;
163                }
164            } else {
165                propagation_reason.push(predicate![var <= rhs_lb - 1]);
166            }
167        }
168        // If there is exactly one variable UB(a_i) >= LB(rhs, constraint_tag }, then the
169        // propagating variable is Some. In that case, intersect the bounds of that variable
170        // and the rhs. Given previous rules, only the lower bound of the propagated
171        // variable needs to be propagated.
172        if let Some(propagating_variable) = propagating_variable {
173            let var_lb = context.lower_bound(propagating_variable);
174            if var_lb < rhs_lb {
175                propagation_reason.push(predicate![self.rhs >= rhs_lb]);
176                context.post(
177                    predicate![propagating_variable >= rhs_lb],
178                    (propagation_reason, &self.inference_code),
179                )?;
180            }
181        }
182
183        Ok(())
184    }
185}
186
187#[derive(Clone, Debug)]
188pub struct MaximumChecker<ElementVar, Rhs> {
189    pub array: Box<[ElementVar]>,
190    pub rhs: Rhs,
191}
192
193impl<ElementVar, Rhs, Atomic> InferenceChecker<Atomic> for MaximumChecker<ElementVar, Rhs>
194where
195    Atomic: AtomicConstraint,
196    ElementVar: CheckerVariable<Atomic>,
197    Rhs: CheckerVariable<Atomic>,
198{
199    fn check(
200        &self,
201        state: pumpkin_checking::VariableState<Atomic>,
202        _: &[Atomic],
203        _: Option<&Atomic>,
204    ) -> bool {
205        let lowest_maximum = self
206            .array
207            .iter()
208            .map(|element| element.induced_lower_bound(&state))
209            .max()
210            .unwrap_or(IntExt::NegativeInf);
211        let highest_maximum = self
212            .array
213            .iter()
214            .map(|element| element.induced_upper_bound(&state))
215            .max()
216            .unwrap_or(IntExt::PositiveInf);
217
218        // If the intersection between the domain of `rhs` and `[lowest_maximum,
219        // highest_maximum]` is empty, there is a conflict.
220
221        lowest_maximum > self.rhs.induced_upper_bound(&state)
222            || highest_maximum < self.rhs.induced_lower_bound(&state)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use pumpkin_core::predicate;
229    use pumpkin_core::predicates::Predicate;
230    use pumpkin_core::predicates::PropositionalConjunction;
231    use pumpkin_core::propagation::CurrentNogood;
232    use pumpkin_core::state::State;
233
234    use super::*;
235    use crate::StateExt;
236
237    #[test]
238    fn upper_bound_of_rhs_matches_maximum_upper_bound_of_array_at_initialise() {
239        let mut state = State::default();
240
241        let a = state.new_interval_variable(1, 3, None);
242        let b = state.new_interval_variable(1, 4, None);
243        let c = state.new_interval_variable(1, 5, None);
244
245        let rhs = state.new_interval_variable(1, 10, None);
246        let constraint_tag = state.new_constraint_tag();
247
248        let _ = state.add_propagator(MaximumArgs {
249            array: [a, b, c].into(),
250            rhs,
251            constraint_tag,
252        });
253        state.propagate_to_fixed_point().expect("no empty domain");
254
255        state.assert_bounds(rhs, 1, 5);
256
257        let mut reason_buffer: Vec<Predicate> = vec![];
258        let _ = state.get_propagation_reason(
259            predicate![rhs <= 5],
260            &mut reason_buffer,
261            CurrentNogood::empty(),
262        );
263        let reason: PropositionalConjunction = reason_buffer.into();
264        assert_eq!(conjunction!([a <= 5] & [b <= 5] & [c <= 5]), reason);
265    }
266
267    #[test]
268    fn lower_bound_of_rhs_is_maximum_of_lower_bounds_in_array() {
269        let mut state = State::default();
270
271        let a = state.new_interval_variable(3, 10, None);
272        let b = state.new_interval_variable(4, 10, None);
273        let c = state.new_interval_variable(5, 10, None);
274
275        let rhs = state.new_interval_variable(1, 10, None);
276        let constraint_tag = state.new_constraint_tag();
277
278        let _ = state.add_propagator(MaximumArgs {
279            array: [a, b, c].into(),
280            rhs,
281            constraint_tag,
282        });
283        state.propagate_to_fixed_point().expect("no empty domain");
284
285        state.assert_bounds(rhs, 5, 10);
286
287        let mut reason_buffer: Vec<Predicate> = vec![];
288        let _ = state.get_propagation_reason(
289            predicate![rhs >= 5],
290            &mut reason_buffer,
291            CurrentNogood::empty(),
292        );
293        let reason: PropositionalConjunction = reason_buffer.into();
294        assert_eq!(conjunction!([c >= 5]), reason);
295    }
296
297    #[test]
298    fn upper_bound_of_all_array_elements_at_most_rhs_max_at_initialise() {
299        let mut state = State::default();
300
301        let array = (1..=5)
302            .map(|idx| state.new_interval_variable(1, 4 + idx, None))
303            .collect::<Box<_>>();
304
305        let rhs = state.new_interval_variable(1, 3, None);
306        let constraint_tag = state.new_constraint_tag();
307
308        let _ = state.add_propagator(MaximumArgs {
309            array: array.clone(),
310            rhs,
311            constraint_tag,
312        });
313        state.propagate_to_fixed_point().expect("no empty domain");
314
315        for var in array.iter() {
316            state.assert_bounds(*var, 1, 3);
317
318            let mut reason_buffer: Vec<Predicate> = vec![];
319            let _ = state.get_propagation_reason(
320                predicate![var <= 3],
321                &mut reason_buffer,
322                CurrentNogood::empty(),
323            );
324            let reason: PropositionalConjunction = reason_buffer.into();
325            assert_eq!(conjunction!([rhs <= 3]), reason);
326        }
327    }
328
329    #[test]
330    fn single_variable_propagate() {
331        let mut state = State::default();
332
333        let array = (1..=5)
334            .map(|idx| state.new_interval_variable(1, 1 + 10 * idx, None))
335            .collect::<Box<_>>();
336
337        let rhs = state.new_interval_variable(45, 60, None);
338        let constraint_tag = state.new_constraint_tag();
339
340        let _ = state.add_propagator(MaximumArgs {
341            array: array.clone(),
342            rhs,
343            constraint_tag,
344        });
345        state.propagate_to_fixed_point().expect("no empty domain");
346
347        state.assert_bounds(*array.last().unwrap(), 45, 51);
348        state.assert_bounds(rhs, 45, 51);
349    }
350}