Skip to main content

pumpkin_propagators/propagators/arithmetic/
integer_division.rs

1use pumpkin_checking::AtomicConstraint;
2use pumpkin_checking::CheckerVariable;
3use pumpkin_checking::InferenceChecker;
4use pumpkin_checking::IntExt;
5use pumpkin_core::asserts::pumpkin_assert_simple;
6use pumpkin_core::conjunction;
7use pumpkin_core::declare_inference_label;
8use pumpkin_core::predicate;
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/// The [`PropagatorConstructor`] for the [`DivisionPropagator`].
24#[derive(Clone, Debug)]
25pub struct DivisionArgs<VA, VB, VC> {
26    pub numerator: VA,
27    pub denominator: VB,
28    pub rhs: VC,
29    pub constraint_tag: ConstraintTag,
30}
31
32const ID_NUMERATOR: LocalId = LocalId::from(0);
33const ID_DENOMINATOR: LocalId = LocalId::from(1);
34const ID_RHS: LocalId = LocalId::from(2);
35
36declare_inference_label!(Division);
37
38impl<VA, VB, VC> PropagatorConstructor for DivisionArgs<VA, VB, VC>
39where
40    VA: IntegerVariable + 'static,
41    VB: IntegerVariable + 'static,
42    VC: IntegerVariable + 'static,
43{
44    type PropagatorImpl = DivisionPropagator<VA, VB, VC>;
45
46    fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
47        let DivisionArgs {
48            numerator,
49            denominator,
50            rhs,
51            constraint_tag,
52        } = self;
53
54        pumpkin_assert_simple!(
55            !context.contains(&denominator, 0),
56            "Denominator cannot contain 0"
57        );
58
59        context.register(numerator.clone(), DomainEvents::BOUNDS, ID_NUMERATOR);
60        context.register(denominator.clone(), DomainEvents::BOUNDS, ID_DENOMINATOR);
61        context.register(rhs.clone(), DomainEvents::BOUNDS, ID_RHS);
62
63        let inference_code = InferenceCode::new(constraint_tag, Division);
64
65        DivisionPropagator {
66            numerator,
67            denominator,
68            rhs,
69            inference_code,
70        }
71    }
72
73    fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
74        checkers.add_inference_checker(
75            InferenceCode::new(self.constraint_tag, Division),
76            Box::new(IntegerDivisionChecker {
77                numerator: self.numerator.clone(),
78                denominator: self.denominator.clone(),
79                rhs: self.rhs.clone(),
80            }),
81        );
82    }
83}
84
85/// A propagator for maintaining the constraint `numerator / denominator = rhs`; note that this
86/// propagator performs truncating division (i.e. rounding towards 0).
87///
88/// The propagator assumes that the `denominator` is a (non-zero) number.
89///
90/// The implementation is ported from [OR-tools](https://github.com/google/or-tools/blob/870edf6f7bff6b8ff0d267d936be7e331c5b8c2d/ortools/sat/integer_expr.cc#L1209C1-L1209C19).
91#[derive(Clone, Debug)]
92pub struct DivisionPropagator<VA, VB, VC> {
93    numerator: VA,
94    denominator: VB,
95    rhs: VC,
96    inference_code: InferenceCode,
97}
98
99impl<VA: 'static, VB: 'static, VC: 'static> Propagator for DivisionPropagator<VA, VB, VC>
100where
101    VA: IntegerVariable,
102    VB: IntegerVariable,
103    VC: IntegerVariable,
104{
105    fn priority(&self) -> Priority {
106        Priority::High
107    }
108
109    fn name(&self) -> &str {
110        "Division"
111    }
112
113    fn propagate_from_scratch(&self, context: PropagationContext) -> PropagationStatusCP {
114        perform_propagation(
115            context,
116            &self.numerator,
117            &self.denominator,
118            &self.rhs,
119            &self.inference_code,
120        )
121    }
122}
123
124fn perform_propagation<VA: IntegerVariable, VB: IntegerVariable, VC: IntegerVariable>(
125    mut context: PropagationContext,
126    numerator: &VA,
127    denominator: &VB,
128    rhs: &VC,
129    inference_code: &InferenceCode,
130) -> PropagationStatusCP {
131    if context.lower_bound(denominator) < 0 && context.upper_bound(denominator) > 0 {
132        // For now we don't do anything in this case, note that this will not lead to incorrect
133        // behaviour since any solution to this constraint will necessarily have to fix the
134        // denominator.
135        return Ok(());
136    }
137
138    let mut negated_numerator = &numerator.scaled(-1);
139    let mut numerator = &numerator.scaled(1);
140
141    let mut negated_denominator = &denominator.scaled(-1);
142    let mut denominator = &denominator.scaled(1);
143
144    if context.upper_bound(denominator) < 0 {
145        // If the denominator is negative then we swap the numerator with its negated version and we
146        // swap the denominator with its negated version.
147        std::mem::swap(&mut numerator, &mut negated_numerator);
148        std::mem::swap(&mut denominator, &mut negated_denominator);
149    }
150
151    let negated_rhs = &rhs.scaled(-1);
152
153    // We propagate the domains to their appropriate signs (e.g. if the numerator is negative and
154    // the denominator is positive then the rhs should also be negative)
155    propagate_signs(&mut context, numerator, denominator, rhs, inference_code)?;
156
157    // If the upper-bound of the numerator is positive and the upper-bound of the rhs is positive
158    // then we can simply update the upper-bounds
159    if context.upper_bound(numerator) >= 0 && context.upper_bound(rhs) >= 0 {
160        propagate_upper_bounds(&mut context, numerator, denominator, rhs, inference_code)?;
161    }
162
163    // If the lower-bound of the numerator is negative and the lower-bound of the rhs is negative
164    // then we negate these variables and update the upper-bounds
165    if context.upper_bound(negated_numerator) >= 0 && context.upper_bound(negated_rhs) >= 0 {
166        propagate_upper_bounds(
167            &mut context,
168            negated_numerator,
169            denominator,
170            negated_rhs,
171            inference_code,
172        )?;
173    }
174
175    // If the domain of the numerator is positive and the domain of the rhs is positive (and we know
176    // that our denominator is positive) then we can propagate based on the assumption that all the
177    // domains are positive
178    if context.lower_bound(numerator) >= 0 && context.lower_bound(rhs) >= 0 {
179        propagate_positive_domains(&mut context, numerator, denominator, rhs, inference_code)?;
180    }
181
182    // If the domain of the numerator is negative and the domain of the rhs is negative (and we know
183    // that our denominator is positive) then we propagate based on the views over the numerator and
184    // rhs
185    if context.lower_bound(negated_numerator) >= 0 && context.lower_bound(negated_rhs) >= 0 {
186        propagate_positive_domains(
187            &mut context,
188            negated_numerator,
189            denominator,
190            negated_rhs,
191            inference_code,
192        )?;
193    }
194
195    Ok(())
196}
197
198/// Propagates the domains of variables if all the domains are positive (if the variables are
199/// sign-fixed then we simply transform them to positive domains using [`AffineView`]s); it performs
200/// the following propagations:
201/// - The minimum value that division can take on is the smallest value that `numerator /
202///   denominator` can take on
203/// - The numerator is at least as large as the smallest value that `denominator * rhs` can take on
204/// - The value of the denominator is smaller than the largest value that `numerator / rhs` can take
205///   on
206/// - The denominator is at least as large as the ratio between the largest ceiled ratio between
207///   `numerator + 1` and `rhs + 1`
208fn propagate_positive_domains<VA: IntegerVariable, VB: IntegerVariable, VC: IntegerVariable>(
209    context: &mut PropagationContext,
210    numerator: &VA,
211    denominator: &VB,
212    rhs: &VC,
213    inference_code: &InferenceCode,
214) -> PropagationStatusCP {
215    let rhs_min = context.lower_bound(rhs);
216    let rhs_max = context.upper_bound(rhs);
217    let numerator_min = context.lower_bound(numerator);
218    let numerator_max = context.upper_bound(numerator);
219    let denominator_min = context.lower_bound(denominator);
220    let denominator_max = context.upper_bound(denominator);
221
222    // The new minimum value of the rhs is the minimum value that the division can take on
223    let new_min_rhs = numerator_min / denominator_max;
224    if rhs_min < new_min_rhs {
225        context.post(
226            predicate![rhs >= new_min_rhs],
227            (
228                conjunction!(
229                    [numerator >= numerator_min]
230                        & [denominator <= denominator_max]
231                        & [denominator >= 1]
232                ),
233                inference_code,
234            ),
235        )?;
236    }
237
238    // numerator / denominator >= rhs_min
239    // numerator >= rhs_min * denominator
240    // numerator >= rhs_min * denominator_min
241    // Note that we use rhs_min rather than new_min_rhs, this appears to be a heuristic
242    let new_min_numerator = denominator_min * rhs_min;
243    if numerator_min < new_min_numerator {
244        context.post(
245            predicate![numerator >= new_min_numerator],
246            (
247                conjunction!([denominator >= denominator_min] & [rhs >= rhs_min]),
248                inference_code,
249            ),
250        )?;
251    }
252
253    // numerator / denominator >= rhs_min
254    // numerator >= rhs_min * denominator
255    // If rhs_min == 0 -> no propagations
256    // Otherwise, denominator <= numerator / rhs_min & denominator <= numerator_max / rhs_min
257    if rhs_min > 0 {
258        let new_max_denominator = numerator_max / rhs_min;
259        if denominator_max > new_max_denominator {
260            context.post(
261                predicate![denominator <= new_max_denominator],
262                (
263                    conjunction!(
264                        [numerator <= numerator_max]
265                            & [numerator >= 0]
266                            & [rhs >= rhs_min]
267                            & [denominator >= 1]
268                    ),
269                    inference_code,
270                ),
271            )?;
272        }
273    }
274
275    let new_min_denominator = {
276        // Called the CeilRatio in OR-tools
277        let dividend = numerator_min + 1;
278        let positive_divisor = rhs_max + 1;
279
280        let result = dividend / positive_divisor;
281        let adjust = result * positive_divisor < dividend;
282        result + adjust as i32
283    };
284
285    if denominator_min < new_min_denominator {
286        context.post(
287            predicate![denominator >= new_min_denominator],
288            (
289                conjunction!(
290                    [numerator >= numerator_min]
291                        & [rhs <= rhs_max]
292                        & [rhs >= 0]
293                        & [denominator >= 1]
294                ),
295                inference_code,
296            ),
297        )?;
298    }
299
300    Ok(())
301}
302
303/// Propagates the upper-bounds of the right-hand side and the numerator, it performs the following
304/// propagations
305/// - The maximum value of the right-hand side can only be as large as the largest value that
306///   `numerator / denominator` can take on
307/// - The maximum value of the numerator is smaller than `(ub(rhs) + 1) * denominator - 1`, note
308///   that this might not be the most constrictive bound
309fn propagate_upper_bounds<VA: IntegerVariable, VB: IntegerVariable, VC: IntegerVariable>(
310    context: &mut PropagationContext,
311    numerator: &VA,
312    denominator: &VB,
313    rhs: &VC,
314    inference_code: &InferenceCode,
315) -> PropagationStatusCP {
316    let rhs_max = context.upper_bound(rhs);
317    let numerator_max = context.upper_bound(numerator);
318    let denominator_min = context.lower_bound(denominator);
319    let denominator_max = context.upper_bound(denominator);
320
321    // The new maximum value of the rhs is the maximum value that the division can take on (note
322    // that numerator_max is positive and denominator_min is also positive)
323    let new_max_rhs = numerator_max / denominator_min;
324    if rhs_max > new_max_rhs {
325        context.post(
326            predicate![rhs <= new_max_rhs],
327            (
328                conjunction!([numerator <= numerator_max] & [denominator >= denominator_min]),
329                inference_code,
330            ),
331        )?;
332    }
333
334    // numerator / denominator <= rhs.max
335    // numerator < (rhs.max + 1) * denominator
336    // numerator + 1 <= (rhs.max + 1) * denominator.max
337    // numerator <= (rhs.max + 1) * denominator.max - 1
338    // Note that we use rhs_max here rather than the new upper-bound, this appears to be a heuristic
339    let new_max_numerator = (rhs_max + 1) * denominator_max - 1;
340    if numerator_max > new_max_numerator {
341        context.post(
342            predicate![numerator <= new_max_numerator],
343            (
344                conjunction!(
345                    [denominator <= denominator_max] & [denominator >= 1] & [rhs <= rhs_max]
346                ),
347                inference_code,
348            ),
349        )?;
350    }
351
352    Ok(())
353}
354
355/// Propagates the signs of the variables, more specifically, it performs the following propagations
356/// (assuming that the denominator is always > 0):
357/// - If the numerator is non-negative then the right-hand side must be non-negative as well
358/// - If the right-hand side is positive then the numerator must be positive as well
359/// - If the numerator is non-positive then the right-hand side must be non-positive as well
360/// - If the right-hand is negative then the numerator must be negative as well
361fn propagate_signs<VA: IntegerVariable, VB: IntegerVariable, VC: IntegerVariable>(
362    context: &mut PropagationContext,
363    numerator: &VA,
364    denominator: &VB,
365    rhs: &VC,
366    inference_code: &InferenceCode,
367) -> PropagationStatusCP {
368    let rhs_min = context.lower_bound(rhs);
369    let rhs_max = context.upper_bound(rhs);
370    let numerator_min = context.lower_bound(numerator);
371    let numerator_max = context.upper_bound(numerator);
372
373    // First we propagate the signs
374    // If the numerator >= 0 (and we know that denominator > 0) then the rhs must be >= 0
375    if numerator_min >= 0 && rhs_min < 0 {
376        context.post(
377            predicate![rhs >= 0],
378            (
379                conjunction!([numerator >= 0] & [denominator >= 1]),
380                inference_code,
381            ),
382        )?;
383    }
384
385    // If rhs > 0 (and we know that denominator > 0) then the numerator must be > 0
386    if numerator_min <= 0 && rhs_min > 0 {
387        context.post(
388            predicate![numerator >= 1],
389            (
390                conjunction!([rhs >= 1] & [denominator >= 1]),
391                inference_code,
392            ),
393        )?;
394    }
395
396    // If numerator <= 0 (and we know that denominator > 0) then the rhs must be <= 0
397    if numerator_max <= 0 && rhs_max > 0 {
398        context.post(
399            predicate![rhs <= 0],
400            (
401                conjunction!([numerator <= 0] & [denominator >= 1]),
402                inference_code,
403            ),
404        )?;
405    }
406
407    // If the rhs < 0 (and we know that denominator > 0) then the numerator must be < 0
408    if numerator_max >= 0 && rhs_max < 0 {
409        context.post(
410            predicate![numerator <= -1],
411            (
412                conjunction!([rhs <= -1] & [denominator >= 1]),
413                inference_code,
414            ),
415        )?;
416    }
417
418    Ok(())
419}
420
421#[derive(Clone, Debug)]
422pub struct IntegerDivisionChecker<VA, VB, VC> {
423    pub numerator: VA,
424    pub denominator: VB,
425    pub rhs: VC,
426}
427
428impl<VA, VB, VC, Atomic> InferenceChecker<Atomic> for IntegerDivisionChecker<VA, VB, VC>
429where
430    Atomic: AtomicConstraint,
431    VA: CheckerVariable<Atomic>,
432    VB: CheckerVariable<Atomic>,
433    VC: CheckerVariable<Atomic>,
434{
435    fn check(
436        &self,
437        state: pumpkin_checking::VariableState<Atomic>,
438        _premises: &[Atomic],
439        _consequent: Option<&Atomic>,
440    ) -> bool {
441        // We apply interval arithmetic to determine that the computed interval `a div b`
442        // does not intersect with the domain of `c`.
443        //
444        // See https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators.
445
446        let x1 = self.numerator.induced_lower_bound(&state);
447        let x2 = self.numerator.induced_upper_bound(&state);
448        let y1 = self.denominator.induced_lower_bound(&state);
449        let y2 = self.denominator.induced_upper_bound(&state);
450
451        assert!(
452            y2 < 0 || y1 > 0,
453            "Currentl, the checker does not contain inferences where the denominator spans 0"
454        );
455
456        let computed_c_lower: IntExt = *[
457            x1.div_ceil(y1),
458            x1.div_ceil(y2),
459            x2.div_ceil(y1),
460            x2.div_ceil(y2),
461        ]
462        .iter()
463        .flatten()
464        .min()
465        .expect("Expected at least one element to be defined");
466
467        let computed_c_upper: IntExt = *[
468            x1.div_floor(y1),
469            x1.div_floor(y2),
470            x2.div_floor(y1),
471            x2.div_floor(y2),
472        ]
473        .iter()
474        .flatten()
475        .min()
476        .expect("Expected at least one element to be defined");
477
478        let c_lower = self.rhs.induced_lower_bound(&state);
479        let c_upper = self.rhs.induced_upper_bound(&state);
480
481        computed_c_upper < c_lower || computed_c_lower > c_upper
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use pumpkin_core::state::State;
488
489    use super::*;
490
491    #[test]
492    fn detects_conflicts() {
493        let mut state = State::default();
494        let numerator = state.new_interval_variable(1, 1, None);
495        let denominator = state.new_interval_variable(2, 2, None);
496        let rhs = state.new_interval_variable(2, 2, None);
497        let constraint_tag = state.new_constraint_tag();
498
499        let _ = state.add_propagator(DivisionArgs {
500            numerator,
501            denominator,
502            rhs,
503            constraint_tag,
504        });
505
506        let _ = state.propagate_to_fixed_point().unwrap_err();
507    }
508}