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