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