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