Skip to main content

pumpkin_propagators/propagators/arithmetic/
absolute_value.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::proof::ConstraintTag;
9use pumpkin_core::proof::InferenceCode;
10use pumpkin_core::propagation::DomainEvents;
11use pumpkin_core::propagation::InferenceCheckers;
12use pumpkin_core::propagation::LocalId;
13use pumpkin_core::propagation::Priority;
14use pumpkin_core::propagation::PropagationContext;
15use pumpkin_core::propagation::Propagator;
16use pumpkin_core::propagation::PropagatorConstructor;
17use pumpkin_core::propagation::PropagatorConstructorContext;
18use pumpkin_core::propagation::ReadDomains;
19use pumpkin_core::state::PropagationStatusCP;
20use pumpkin_core::variables::IntegerVariable;
21
22declare_inference_label!(AbsoluteValue);
23
24#[derive(Clone, Debug)]
25pub struct AbsoluteValueArgs<VA, VB> {
26    pub signed: VA,
27    pub absolute: VB,
28    pub constraint_tag: ConstraintTag,
29}
30
31impl<VA, VB> PropagatorConstructor for AbsoluteValueArgs<VA, VB>
32where
33    VA: IntegerVariable + 'static,
34    VB: IntegerVariable + 'static,
35{
36    type PropagatorImpl = AbsoluteValuePropagator<VA, VB>;
37
38    fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
39        checkers.add_inference_checker(
40            InferenceCode::new(self.constraint_tag, AbsoluteValue),
41            Box::new(AbsoluteValueChecker {
42                signed: self.signed.clone(),
43                absolute: self.absolute.clone(),
44            }),
45        );
46    }
47
48    fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
49        let AbsoluteValueArgs {
50            signed,
51            absolute,
52            constraint_tag,
53        } = self;
54
55        context.register(signed.clone(), DomainEvents::BOUNDS, LocalId::from(0));
56        context.register(absolute.clone(), DomainEvents::BOUNDS, LocalId::from(1));
57
58        let inference_code = InferenceCode::new(constraint_tag, AbsoluteValue);
59
60        AbsoluteValuePropagator {
61            signed,
62            absolute,
63            inference_code,
64        }
65    }
66}
67
68/// Propagator for `absolute = |signed|`, where `absolute` and `signed` are integer variables.
69///
70/// The propagator is bounds consistent wrt signed. That means that if `signed \in {-2, -1, 1, 2}`,
71/// the propagator will not propagate `[absolute >= 1]`.
72#[derive(Clone, Debug)]
73pub struct AbsoluteValuePropagator<VA, VB> {
74    signed: VA,
75    absolute: VB,
76    inference_code: InferenceCode,
77}
78
79impl<VA, VB> Propagator for AbsoluteValuePropagator<VA, VB>
80where
81    VA: IntegerVariable + 'static,
82    VB: IntegerVariable + 'static,
83{
84    fn priority(&self) -> Priority {
85        Priority::High
86    }
87
88    fn name(&self) -> &str {
89        "IntAbs"
90    }
91
92    fn propagate_from_scratch(&self, mut context: PropagationContext) -> PropagationStatusCP {
93        // The bound of absolute may be tightened further during propagation, but it is at least
94        // zero at the root.
95        context.post(
96            predicate![self.absolute >= 0],
97            (conjunction!(), &self.inference_code),
98        )?;
99
100        // Propagating absolute value can be broken into a few cases:
101        // - `signed` is sign-fixed (i.e. `upper_bound <= 0` or `lower_bound >= 0`), in which case
102        //   the bounds of `signed` can be propagated to `absolute` (taking care of swapping bounds
103        //   when the `signed` is negative).
104        // - `signed` is not sign-fixed (i.e. `lower_bound <= 0` and `upper_bound >= 0`), in which
105        //   case the lower bound of `absolute` cannot be tightened without looking into specific
106        //   domain values for `signed`, which we don't do.
107        let signed_lb = context.lower_bound(&self.signed);
108        let signed_ub = context.upper_bound(&self.signed);
109
110        let signed_absolute_ub = i32::max(signed_lb.abs(), signed_ub.abs());
111
112        context.post(
113            predicate![self.absolute <= signed_absolute_ub],
114            (
115                conjunction!([self.signed >= signed_lb] & [self.signed <= signed_ub]),
116                &self.inference_code,
117            ),
118        )?;
119
120        if signed_lb > 0 {
121            context.post(
122                predicate![self.absolute >= signed_lb],
123                (
124                    conjunction!([self.signed >= signed_lb]),
125                    &self.inference_code,
126                ),
127            )?;
128        } else if signed_ub < 0 {
129            context.post(
130                predicate![self.absolute >= signed_ub.abs()],
131                (
132                    conjunction!([self.signed <= signed_ub]),
133                    &self.inference_code,
134                ),
135            )?;
136        }
137
138        let absolute_ub = context.upper_bound(&self.absolute);
139        let absolute_lb = context.lower_bound(&self.absolute);
140        context.post(
141            predicate![self.signed >= -absolute_ub],
142            (
143                conjunction!([self.absolute <= absolute_ub]),
144                &self.inference_code,
145            ),
146        )?;
147        context.post(
148            predicate![self.signed <= absolute_ub],
149            (
150                conjunction!([self.absolute <= absolute_ub]),
151                &self.inference_code,
152            ),
153        )?;
154
155        if signed_ub <= 0 {
156            context.post(
157                predicate![self.signed <= -absolute_lb],
158                (
159                    conjunction!([self.signed <= 0] & [self.absolute >= absolute_lb]),
160                    &self.inference_code,
161                ),
162            )?;
163        } else if signed_lb >= 0 {
164            context.post(
165                predicate![self.signed >= absolute_lb],
166                (
167                    conjunction!([self.signed >= 0] & [self.absolute >= absolute_lb]),
168                    &self.inference_code,
169                ),
170            )?;
171        }
172
173        Ok(())
174    }
175}
176
177#[derive(Clone, Debug)]
178pub struct AbsoluteValueChecker<VA, VB> {
179    signed: VA,
180    absolute: VB,
181}
182
183impl<VA, VB, Atomic> InferenceChecker<Atomic> for AbsoluteValueChecker<VA, VB>
184where
185    VA: CheckerVariable<Atomic>,
186    VB: CheckerVariable<Atomic>,
187    Atomic: AtomicConstraint,
188{
189    fn check(
190        &self,
191        state: pumpkin_checking::VariableState<Atomic>,
192        _: &[Atomic],
193        _: Option<&Atomic>,
194    ) -> bool {
195        let signed_lower = self.signed.induced_lower_bound(&state);
196        let signed_upper = self.signed.induced_upper_bound(&state);
197        let absolute_lower = self.absolute.induced_lower_bound(&state);
198        let absolute_upper = self.absolute.induced_upper_bound(&state);
199
200        if absolute_lower < 0 {
201            // The absolute value cannot have negative values.
202            return true;
203        }
204
205        // Now we compute the interval for |signed| based on the domain of signed.
206        let (computed_signed_lower, computed_signed_upper) = if signed_lower >= 0 {
207            (signed_lower, signed_upper)
208        } else if signed_upper <= 0 {
209            (-signed_upper, -signed_lower)
210        } else if signed_lower < 0 && 0_i32 < signed_upper {
211            (IntExt::Int(0), std::cmp::max(-signed_lower, signed_upper))
212        } else {
213            unreachable!()
214        };
215
216        // The intervals should not match, otherwise there is no conflict.
217        computed_signed_lower != absolute_lower || computed_signed_upper != absolute_upper
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use pumpkin_core::state::State;
224
225    use super::*;
226    use crate::StateExt;
227
228    #[test]
229    fn absolute_bounds_are_propagated_at_initialise() {
230        let mut state = State::default();
231
232        let signed = state.new_interval_variable(-3, 4, None);
233        let absolute = state.new_interval_variable(-2, 10, None);
234        let constraint_tag = state.new_constraint_tag();
235
236        let _ = state.add_propagator(AbsoluteValueArgs {
237            signed,
238            absolute,
239            constraint_tag,
240        });
241        state.propagate_to_fixed_point().expect("no empty domains");
242
243        state.assert_bounds(absolute, 0, 4);
244    }
245
246    #[test]
247    fn signed_bounds_are_propagated_at_initialise() {
248        let mut state = State::default();
249
250        let signed = state.new_interval_variable(-5, 5, None);
251        let absolute = state.new_interval_variable(0, 3, None);
252        let constraint_tag = state.new_constraint_tag();
253
254        let _ = state.add_propagator(AbsoluteValueArgs {
255            signed,
256            absolute,
257            constraint_tag,
258        });
259        state.propagate_to_fixed_point().expect("no empty domains");
260
261        state.assert_bounds(signed, -3, 3);
262    }
263
264    #[test]
265    fn absolute_lower_bound_can_be_strictly_positive() {
266        let mut state = State::default();
267
268        let signed = state.new_interval_variable(3, 6, None);
269        let absolute = state.new_interval_variable(0, 10, None);
270        let constraint_tag = state.new_constraint_tag();
271
272        let _ = state.add_propagator(AbsoluteValueArgs {
273            signed,
274            absolute,
275            constraint_tag,
276        });
277        state.propagate_to_fixed_point().expect("no empty domains");
278
279        state.assert_bounds(absolute, 3, 6);
280    }
281
282    #[test]
283    fn strictly_negative_signed_value_can_propagate_lower_bound_on_absolute() {
284        let mut state = State::default();
285
286        let signed = state.new_interval_variable(-5, -3, None);
287        let absolute = state.new_interval_variable(1, 5, None);
288        let constraint_tag = state.new_constraint_tag();
289
290        let _ = state.add_propagator(AbsoluteValueArgs {
291            signed,
292            absolute,
293            constraint_tag,
294        });
295        state.propagate_to_fixed_point().expect("no empty domains");
296
297        state.assert_bounds(absolute, 3, 5);
298    }
299
300    #[test]
301    fn lower_bound_on_absolute_can_propagate_negative_upper_bound_on_signed() {
302        let mut state = State::default();
303
304        let signed = state.new_interval_variable(-5, 0, None);
305        let absolute = state.new_interval_variable(1, 5, None);
306        let constraint_tag = state.new_constraint_tag();
307
308        let _ = state.add_propagator(AbsoluteValueArgs {
309            signed,
310            absolute,
311            constraint_tag,
312        });
313        state.propagate_to_fixed_point().expect("no empty domains");
314
315        state.assert_bounds(signed, -5, -1);
316    }
317
318    #[test]
319    fn lower_bound_on_absolute_can_propagate_positive_lower_bound_on_signed() {
320        let mut state = State::default();
321
322        let signed = state.new_interval_variable(1, 5, None);
323        let absolute = state.new_interval_variable(3, 5, None);
324        let constraint_tag = state.new_constraint_tag();
325
326        let _ = state.add_propagator(AbsoluteValueArgs {
327            signed,
328            absolute,
329            constraint_tag,
330        });
331        state.propagate_to_fixed_point().expect("no empty domains");
332
333        state.assert_bounds(signed, 3, 5);
334    }
335}