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::results::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]),
110                &self.inference_code,
111            )?;
112
113            let var_lb = context.lower_bound(var);
114            let var_ub = context.upper_bound(var);
115
116            if var_lb > max_lb {
117                max_lb = var_lb;
118                lb_reason = predicate![var >= var_lb];
119            }
120
121            if var_ub > max_ub {
122                max_ub = var_ub;
123            }
124        }
125        // Rule 2.
126        // LB(rhs, constraint_tag } >= max{LB(a_i)}.
127        context.post(
128            predicate![self.rhs >= max_lb],
129            PropositionalConjunction::from(lb_reason),
130            &self.inference_code,
131        )?;
132
133        // Rule 3.
134        // UB(rhs, constraint_tag } <= max{UB(a_i)}.
135        // Note that this implicitly also covers the rule:
136        // 'if LB(rhs, constraint_tag } > UB(a_i) for all i, then conflict'.
137        if rhs_ub > max_ub {
138            let ub_reason: PropositionalConjunction = self
139                .array
140                .iter()
141                .map(|var| predicate![var <= max_ub])
142                .collect();
143            context.post(
144                predicate![self.rhs <= max_ub],
145                ub_reason,
146                &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,
179                    &self.inference_code,
180                )?;
181            }
182        }
183
184        Ok(())
185    }
186}
187
188#[derive(Clone, Debug)]
189pub struct MaximumChecker<ElementVar, Rhs> {
190    pub array: Box<[ElementVar]>,
191    pub rhs: Rhs,
192}
193
194impl<ElementVar, Rhs, Atomic> InferenceChecker<Atomic> for MaximumChecker<ElementVar, Rhs>
195where
196    Atomic: AtomicConstraint,
197    ElementVar: CheckerVariable<Atomic>,
198    Rhs: CheckerVariable<Atomic>,
199{
200    fn check(
201        &self,
202        state: pumpkin_checking::VariableState<Atomic>,
203        _: &[Atomic],
204        _: Option<&Atomic>,
205    ) -> bool {
206        let lowest_maximum = self
207            .array
208            .iter()
209            .map(|element| element.induced_lower_bound(&state))
210            .max()
211            .unwrap_or(IntExt::NegativeInf);
212        let highest_maximum = self
213            .array
214            .iter()
215            .map(|element| element.induced_upper_bound(&state))
216            .max()
217            .unwrap_or(IntExt::PositiveInf);
218
219        // If the intersection between the domain of `rhs` and `[lowest_maximum,
220        // highest_maximum]` is empty, there is a conflict.
221
222        lowest_maximum > self.rhs.induced_upper_bound(&state)
223            || highest_maximum < self.rhs.induced_lower_bound(&state)
224    }
225}
226
227#[allow(deprecated, reason = "Will be refactored")]
228#[cfg(test)]
229mod tests {
230    use pumpkin_core::TestSolver;
231
232    use super::*;
233
234    #[test]
235    fn upper_bound_of_rhs_matches_maximum_upper_bound_of_array_at_initialise() {
236        let mut solver = TestSolver::default();
237
238        let a = solver.new_variable(1, 3);
239        let b = solver.new_variable(1, 4);
240        let c = solver.new_variable(1, 5);
241
242        let rhs = solver.new_variable(1, 10);
243        let constraint_tag = solver.new_constraint_tag();
244
245        let _ = solver
246            .new_propagator(MaximumArgs {
247                array: [a, b, c].into(),
248                rhs,
249                constraint_tag,
250            })
251            .expect("no empty domain");
252
253        solver.assert_bounds(rhs, 1, 5);
254
255        let reason = solver.get_reason_int(predicate![rhs <= 5]);
256        assert_eq!(conjunction!([a <= 5] & [b <= 5] & [c <= 5]), reason);
257    }
258
259    #[test]
260    fn lower_bound_of_rhs_is_maximum_of_lower_bounds_in_array() {
261        let mut solver = TestSolver::default();
262
263        let a = solver.new_variable(3, 10);
264        let b = solver.new_variable(4, 10);
265        let c = solver.new_variable(5, 10);
266
267        let rhs = solver.new_variable(1, 10);
268        let constraint_tag = solver.new_constraint_tag();
269
270        let _ = solver
271            .new_propagator(MaximumArgs {
272                array: [a, b, c].into(),
273                rhs,
274                constraint_tag,
275            })
276            .expect("no empty domain");
277
278        solver.assert_bounds(rhs, 5, 10);
279
280        let reason = solver.get_reason_int(predicate![rhs >= 5]);
281        assert_eq!(conjunction!([c >= 5]), reason);
282    }
283
284    #[test]
285    fn upper_bound_of_all_array_elements_at_most_rhs_max_at_initialise() {
286        let mut solver = TestSolver::default();
287
288        let array = (1..=5)
289            .map(|idx| solver.new_variable(1, 4 + idx))
290            .collect::<Box<_>>();
291
292        let rhs = solver.new_variable(1, 3);
293        let constraint_tag = solver.new_constraint_tag();
294
295        let _ = solver
296            .new_propagator(MaximumArgs {
297                array: array.clone(),
298                rhs,
299                constraint_tag,
300            })
301            .expect("no empty domain");
302
303        for var in array.iter() {
304            solver.assert_bounds(*var, 1, 3);
305            let reason = solver.get_reason_int(predicate![var <= 3]);
306            assert_eq!(conjunction!([rhs <= 3]), reason);
307        }
308    }
309
310    #[test]
311    fn single_variable_propagate() {
312        let mut solver = TestSolver::default();
313
314        let array = (1..=5)
315            .map(|idx| solver.new_variable(1, 1 + 10 * idx))
316            .collect::<Box<_>>();
317
318        let rhs = solver.new_variable(45, 60);
319        let constraint_tag = solver.new_constraint_tag();
320
321        let _ = solver
322            .new_propagator(MaximumArgs {
323                array: array.clone(),
324                rhs,
325                constraint_tag,
326            })
327            .expect("no empty domain");
328
329        solver.assert_bounds(*array.last().unwrap(), 45, 51);
330        solver.assert_bounds(rhs, 45, 51);
331    }
332}