1#![allow(clippy::double_parens, reason = "originates inside the bitfield macro")]
4
5use std::cell::RefCell;
6
7use bitfield_struct::bitfield;
8use pumpkin_checking::AtomicConstraint;
9use pumpkin_checking::CheckerVariable;
10use pumpkin_checking::Domain;
11use pumpkin_checking::InferenceChecker;
12use pumpkin_checking::Union;
13use pumpkin_core::conjunction;
14use pumpkin_core::declare_inference_label;
15use pumpkin_core::predicate;
16use pumpkin_core::predicates::Predicate;
17use pumpkin_core::proof::ConstraintTag;
18use pumpkin_core::proof::InferenceCode;
19use pumpkin_core::propagation::DomainEvents;
20use pumpkin_core::propagation::ExplanationContext;
21use pumpkin_core::propagation::InferenceCheckers;
22use pumpkin_core::propagation::LazyExplanation;
23use pumpkin_core::propagation::LocalId;
24use pumpkin_core::propagation::Priority;
25use pumpkin_core::propagation::PropagationContext;
26use pumpkin_core::propagation::Propagator;
27use pumpkin_core::propagation::PropagatorConstructor;
28use pumpkin_core::propagation::PropagatorConstructorContext;
29use pumpkin_core::propagation::ReadDomains;
30use pumpkin_core::state::PropagationStatusCP;
31use pumpkin_core::variables::IntegerVariable;
32use pumpkin_core::variables::Reason;
33
34#[derive(Clone, Debug)]
35pub struct ElementArgs<VX, VI, VE> {
36 pub array: Box<[VX]>,
37 pub index: VI,
38 pub rhs: VE,
39 pub constraint_tag: ConstraintTag,
40}
41
42declare_inference_label!(Element);
43
44impl<VX, VI, VE> PropagatorConstructor for ElementArgs<VX, VI, VE>
45where
46 VX: IntegerVariable + 'static,
47 VI: IntegerVariable + 'static,
48 VE: IntegerVariable + 'static,
49{
50 type PropagatorImpl = ElementPropagator<VX, VI, VE>;
51
52 fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
53 checkers.add_inference_checker(
54 InferenceCode::new(self.constraint_tag, Element),
55 Box::new(ElementChecker::new(
56 self.array.clone(),
57 self.index.clone(),
58 self.rhs.clone(),
59 )),
60 );
61 }
62
63 fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
64 let ElementArgs {
65 array,
66 index,
67 rhs,
68 constraint_tag,
69 } = self;
70
71 for (i, x_i) in array.iter().enumerate() {
72 context.register(
73 x_i.clone(),
74 DomainEvents::ANY_INT,
75 LocalId::from(i as u32 + ID_X_OFFSET),
76 );
77 }
78
79 context.register(index.clone(), DomainEvents::ANY_INT, ID_INDEX);
80 context.register(rhs.clone(), DomainEvents::ANY_INT, ID_RHS);
81
82 let inference_code = InferenceCode::new(constraint_tag, Element);
83
84 ElementPropagator {
85 array,
86 index,
87 rhs,
88 inference_code,
89 rhs_reason_buffer: vec![],
90 }
91 }
92}
93
94const ID_INDEX: LocalId = LocalId::from(0);
95const ID_RHS: LocalId = LocalId::from(1);
96
97const ID_X_OFFSET: u32 = 2;
99
100#[derive(Clone, Debug)]
105pub struct ElementPropagator<VX, VI, VE> {
106 array: Box<[VX]>,
107 index: VI,
108 rhs: VE,
109 inference_code: InferenceCode,
110
111 rhs_reason_buffer: Vec<Predicate>,
112}
113
114impl<VX, VI, VE> Propagator for ElementPropagator<VX, VI, VE>
115where
116 VX: IntegerVariable + 'static,
117 VI: IntegerVariable + 'static,
118 VE: IntegerVariable + 'static,
119{
120 fn priority(&self) -> Priority {
121 Priority::Low
122 }
123
124 fn name(&self) -> &str {
125 "Element"
126 }
127
128 fn propagate_from_scratch(&self, mut context: PropagationContext) -> PropagationStatusCP {
129 self.propagate_index_bounds_within_array(&mut context)?;
130
131 self.propagate_rhs_bounds_based_on_array(&mut context)?;
132
133 self.propagate_index_based_on_domain_intersection_with_rhs(&mut context)?;
134
135 if let Some(idx) = context.fixed_value(&self.index) {
136 self.propagate_equality(&mut context, idx)?;
137 }
138
139 Ok(())
140 }
141
142 fn lazy_explanation(&mut self, code: u64, context: ExplanationContext) -> LazyExplanation<'_> {
143 let payload = RightHandSideReason::from_bits(code);
144
145 self.rhs_reason_buffer.clear();
146 self.rhs_reason_buffer
147 .extend(self.array.iter().enumerate().map(|(idx, variable)| {
148 if context.contains_at_trail_position(
149 &self.index,
150 idx as i32,
151 context.get_trail_position(),
152 ) {
153 match payload.bound() {
154 Bound::Lower => predicate![variable >= payload.value()],
155 Bound::Upper => predicate![variable <= payload.value()],
156 }
157 } else {
158 predicate![self.index != idx as i32]
159 }
160 }));
161
162 LazyExplanation {
163 predicates: self.rhs_reason_buffer.as_slice(),
164 inference_code: self.inference_code.clone(),
165 }
166 }
167}
168
169impl<VX, VI, VE> ElementPropagator<VX, VI, VE>
170where
171 VX: IntegerVariable + 'static,
172 VI: IntegerVariable + 'static,
173 VE: IntegerVariable + 'static,
174{
175 fn propagate_index_bounds_within_array(
177 &self,
178 context: &mut PropagationContext<'_>,
179 ) -> PropagationStatusCP {
180 context.post(
181 predicate![self.index >= 0],
182 (conjunction!(), &self.inference_code),
183 )?;
184 context.post(
185 predicate![self.index <= self.array.len() as i32 - 1],
186 (conjunction!(), &self.inference_code),
187 )?;
188 Ok(())
189 }
190
191 fn propagate_rhs_bounds_based_on_array(
194 &self,
195 context: &mut PropagationContext<'_>,
196 ) -> PropagationStatusCP {
197 let (rhs_lb, rhs_ub) = self
198 .array
199 .iter()
200 .enumerate()
201 .filter(|(idx, _)| context.contains(&self.index, *idx as i32))
202 .fold((i32::MAX, i32::MIN), |(rhs_lb, rhs_ub), (_, element)| {
203 (
204 i32::min(rhs_lb, context.lower_bound(element)),
205 i32::max(rhs_ub, context.upper_bound(element)),
206 )
207 });
208
209 context.post(
210 predicate![self.rhs >= rhs_lb],
211 Reason::DynamicLazy(
212 RightHandSideReason::new()
213 .with_bound(Bound::Lower)
214 .with_value(rhs_lb)
215 .into_bits(),
216 ),
217 )?;
218 context.post(
219 predicate![self.rhs <= rhs_ub],
220 Reason::DynamicLazy(
221 RightHandSideReason::new()
222 .with_bound(Bound::Upper)
223 .with_value(rhs_ub)
224 .into_bits(),
225 ),
226 )?;
227
228 Ok(())
229 }
230
231 fn propagate_index_based_on_domain_intersection_with_rhs(
234 &self,
235 context: &mut PropagationContext<'_>,
236 ) -> PropagationStatusCP {
237 let rhs_lb = context.lower_bound(&self.rhs);
238 let rhs_ub = context.upper_bound(&self.rhs);
239 let mut to_remove = vec![];
240 for idx in context.iterate_domain(&self.index) {
241 let element = &self.array[idx as usize];
242
243 let element_ub = context.upper_bound(element);
244 let element_lb = context.lower_bound(element);
245
246 let reason = if rhs_lb > element_ub {
247 conjunction!([element <= rhs_lb - 1] & [self.rhs >= rhs_lb])
248 } else if rhs_ub < element_lb {
249 conjunction!([element >= rhs_ub + 1] & [self.rhs <= rhs_ub])
250 } else {
251 continue;
252 };
253
254 to_remove.push((idx, reason));
255 }
256
257 for (idx, reason) in to_remove.drain(..) {
258 context.post(
259 predicate![self.index != idx],
260 (reason, &self.inference_code),
261 )?;
262 }
263
264 Ok(())
265 }
266
267 fn propagate_equality(
270 &self,
271 context: &mut PropagationContext<'_>,
272 index: i32,
273 ) -> PropagationStatusCP {
274 let rhs_lb = context.lower_bound(&self.rhs);
275 let rhs_ub = context.upper_bound(&self.rhs);
276 let lhs = &self.array[index as usize];
277
278 context.post(
279 predicate![lhs >= rhs_lb],
280 (
281 conjunction!([self.rhs >= rhs_lb] & [self.index == index]),
282 &self.inference_code,
283 ),
284 )?;
285 context.post(
286 predicate![lhs <= rhs_ub],
287 (
288 conjunction!([self.rhs <= rhs_ub] & [self.index == index]),
289 &self.inference_code,
290 ),
291 )?;
292 Ok(())
293 }
294}
295
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297#[repr(u8)]
298enum Bound {
299 Lower = 0,
300 Upper = 1,
301}
302
303impl Bound {
304 const fn into_bits(self) -> u8 {
305 self as _
306 }
307
308 const fn from_bits(value: u8) -> Self {
309 match value {
310 0 => Bound::Lower,
311 _ => Bound::Upper,
312 }
313 }
314}
315
316#[bitfield(u64)]
317struct RightHandSideReason {
318 #[bits(32, from = Bound::from_bits)]
319 bound: Bound,
320 value: i32,
321}
322
323#[derive(Clone, Debug)]
324pub struct ElementChecker<VX, VI, VE> {
325 array: Box<[VX]>,
326 index: VI,
327 rhs: VE,
328
329 union: RefCell<Union>,
330}
331
332impl<VX, VI, VE> ElementChecker<VX, VI, VE> {
333 pub fn new(array: Box<[VX]>, index: VI, rhs: VE) -> Self {
335 ElementChecker {
336 array,
337 index,
338 rhs,
339 union: RefCell::new(Union::empty()),
340 }
341 }
342}
343
344impl<VX, VI, VE, Atomic> InferenceChecker<Atomic> for ElementChecker<VX, VI, VE>
345where
346 Atomic: AtomicConstraint,
347 VX: CheckerVariable<Atomic>,
348 VI: CheckerVariable<Atomic>,
349 VE: CheckerVariable<Atomic>,
350{
351 fn check(
352 &self,
353 state: pumpkin_checking::VariableState<Atomic>,
354 _: &[Atomic],
355 _: Option<&Atomic>,
356 ) -> bool {
357 self.union.borrow_mut().reset();
358
359 let supported_elements: Vec<_> = self
367 .array
368 .iter()
369 .enumerate()
370 .filter(|(idx, _)| self.index.induced_domain_contains(&state, *idx as i32))
371 .map(|(_, element)| element)
372 .collect();
373
374 for element in supported_elements {
375 self.union.borrow_mut().add(&state, element);
376 }
377
378 assert!(
379 self.union.borrow().is_consistent(),
380 "at least one element has a non-empty domain or else variable state would be inconsistent"
381 );
382
383 let intersection_lower_bound = self
385 .union
386 .borrow()
387 .lower_bound()
388 .max(self.rhs.induced_lower_bound(&state));
389 let intersection_upper_bound = self
390 .union
391 .borrow()
392 .upper_bound()
393 .min(self.rhs.induced_upper_bound(&state));
394 let holes = self
395 .union
396 .borrow()
397 .holes()
398 .chain(self.rhs.induced_holes(&state))
399 .collect();
400
401 let intersected_domain =
402 Domain::new(intersection_lower_bound, intersection_upper_bound, holes);
403
404 !intersected_domain.is_consistent()
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use pumpkin_checking::TestAtomic;
411 use pumpkin_checking::VariableState;
412 use pumpkin_core::predicate;
413 use pumpkin_core::predicates::Predicate;
414 use pumpkin_core::predicates::PropositionalConjunction;
415 use pumpkin_core::propagation::CurrentNogood;
416 use pumpkin_core::state::State;
417
418 use super::*;
419 use crate::StateExt;
420
421 #[test]
422 fn elements_from_array_with_disjoint_domains_to_rhs_are_filtered_from_index() {
423 let mut state = State::default();
424
425 let x_0 = state.new_interval_variable(4, 6, None);
426 let x_1 = state.new_interval_variable(2, 3, None);
427 let x_2 = state.new_interval_variable(7, 9, None);
428 let x_3 = state.new_interval_variable(14, 15, None);
429
430 let index = state.new_interval_variable(0, 3, None);
431 let rhs = state.new_interval_variable(6, 9, None);
432 let constraint_tag = state.new_constraint_tag();
433
434 let _ = state.add_propagator(ElementArgs {
435 array: vec![x_0, x_1, x_2, x_3].into(),
436 index,
437 rhs,
438 constraint_tag,
439 });
440 state.propagate_to_fixed_point().expect("no empty domains");
441
442 state.assert_bounds(index, 0, 2);
443
444 let mut reason_buffer: Vec<Predicate> = vec![];
445 let _ = state.get_propagation_reason(
446 predicate![index != 3],
447 &mut reason_buffer,
448 CurrentNogood::empty(),
449 );
450 let reason: PropositionalConjunction = reason_buffer.into();
451 assert_eq!(conjunction!([x_3 >= 10] & [rhs <= 9]), reason);
452
453 let mut reason_buffer: Vec<Predicate> = vec![];
454 let _ = state.get_propagation_reason(
455 predicate![index != 1],
456 &mut reason_buffer,
457 CurrentNogood::empty(),
458 );
459 let reason: PropositionalConjunction = reason_buffer.into();
460 assert_eq!(conjunction!([x_1 <= 5] & [rhs >= 6]), reason);
461 }
462
463 #[test]
464 fn bounds_of_rhs_are_min_and_max_of_lower_and_upper_in_array() {
465 let mut state = State::default();
466
467 let x_0 = state.new_interval_variable(3, 10, None);
468 let x_1 = state.new_interval_variable(2, 3, None);
469 let x_2 = state.new_interval_variable(7, 9, None);
470 let x_3 = state.new_interval_variable(14, 15, None);
471
472 let index = state.new_interval_variable(0, 3, None);
473 let rhs = state.new_interval_variable(0, 20, None);
474 let constraint_tag = state.new_constraint_tag();
475
476 let _ = state.add_propagator(ElementArgs {
477 array: vec![x_0, x_1, x_2, x_3].into(),
478 index,
479 rhs,
480 constraint_tag,
481 });
482 state.propagate_to_fixed_point().expect("no empty domains");
483
484 state.assert_bounds(rhs, 2, 15);
485
486 let mut reason_buffer: Vec<Predicate> = vec![];
487 let _ = state.get_propagation_reason(
488 predicate![rhs >= 2],
489 &mut reason_buffer,
490 CurrentNogood::empty(),
491 );
492 let reason: PropositionalConjunction = reason_buffer.into();
493 assert_eq!(
494 conjunction!([x_0 >= 2] & [x_1 >= 2] & [x_2 >= 2] & [x_3 >= 2]),
495 reason
496 );
497
498 let mut reason_buffer: Vec<Predicate> = vec![];
499 let _ = state.get_propagation_reason(
500 predicate![rhs <= 15],
501 &mut reason_buffer,
502 CurrentNogood::empty(),
503 );
504 let reason: PropositionalConjunction = reason_buffer.into();
505 assert_eq!(
506 conjunction!([x_0 <= 15] & [x_1 <= 15] & [x_2 <= 15] & [x_3 <= 15]),
507 reason
508 );
509 }
510
511 #[test]
512 fn fixed_index_propagates_bounds_on_element() {
513 let mut state = State::default();
514
515 let x_0 = state.new_interval_variable(3, 10, None);
516 let x_1 = state.new_interval_variable(0, 15, None);
517 let x_2 = state.new_interval_variable(7, 9, None);
518 let x_3 = state.new_interval_variable(14, 15, None);
519 let constraint_tag = state.new_constraint_tag();
520
521 let index = state.new_interval_variable(1, 1, None);
522 let rhs = state.new_interval_variable(6, 9, None);
523
524 let _ = state.add_propagator(ElementArgs {
525 array: vec![x_0, x_1, x_2, x_3].into(),
526 index,
527 rhs,
528 constraint_tag,
529 });
530 state.propagate_to_fixed_point().expect("no empty domains");
531
532 state.assert_bounds(x_1, 6, 9);
533
534 let mut reason_buffer: Vec<Predicate> = vec![];
535 let _ = state.get_propagation_reason(
536 predicate![x_1 >= 6],
537 &mut reason_buffer,
538 CurrentNogood::empty(),
539 );
540 let reason: PropositionalConjunction = reason_buffer.into();
541 assert_eq!(conjunction!([index == 1] & [rhs >= 6]), reason);
542
543 let mut reason_buffer: Vec<Predicate> = vec![];
544 let _ = state.get_propagation_reason(
545 predicate![x_1 <= 9],
546 &mut reason_buffer,
547 CurrentNogood::empty(),
548 );
549 let reason: PropositionalConjunction = reason_buffer.into();
550 assert_eq!(conjunction!([index == 1] & [rhs <= 9]), reason);
551 }
552
553 #[test]
554 fn index_hole_propagates_bounds_on_rhs() {
555 let mut state = State::default();
556
557 let x_0 = state.new_interval_variable(3, 10, None);
558 let x_1 = state.new_interval_variable(0, 15, None);
559 let x_2 = state.new_interval_variable(7, 9, None);
560 let x_3 = state.new_interval_variable(14, 15, None);
561 let constraint_tag = state.new_constraint_tag();
562
563 let index = state.new_interval_variable(0, 3, None);
564 let _ = state
565 .post(predicate![index != 1])
566 .expect("Value can be removed");
567
568 let rhs = state.new_interval_variable(-10, 30, None);
569
570 let _ = state.add_propagator(ElementArgs {
571 array: vec![x_0, x_1, x_2, x_3].into(),
572 index,
573 rhs,
574 constraint_tag,
575 });
576 state.propagate_to_fixed_point().expect("no empty domains");
577
578 state.assert_bounds(rhs, 3, 15);
579
580 let mut reason_buffer: Vec<Predicate> = vec![];
581 let _ = state.get_propagation_reason(
582 predicate![rhs >= 3],
583 &mut reason_buffer,
584 CurrentNogood::empty(),
585 );
586 let reason: PropositionalConjunction = reason_buffer.into();
587 assert_eq!(
588 conjunction!([x_0 >= 3] & [x_2 >= 3] & [x_3 >= 3] & [index != 1]),
589 reason
590 );
591
592 let mut reason_buffer: Vec<Predicate> = vec![];
593 let _ = state.get_propagation_reason(
594 predicate![rhs <= 15],
595 &mut reason_buffer,
596 CurrentNogood::empty(),
597 );
598 let reason: PropositionalConjunction = reason_buffer.into();
599 assert_eq!(
600 conjunction!([x_0 <= 15] & [x_2 <= 15] & [x_3 <= 15] & [index != 1]),
601 reason
602 );
603 }
604
605 #[test]
606 fn holes_outside_union_bounds_are_ignored() {
607 let premises = [
608 TestAtomic {
609 name: "x1",
610 comparison: pumpkin_checking::Comparison::GreaterEqual,
611 value: 4,
612 },
613 TestAtomic {
614 name: "x2",
615 comparison: pumpkin_checking::Comparison::NotEqual,
616 value: 2,
617 },
618 ];
619
620 let consequent = Some(TestAtomic {
621 name: "x4",
622 comparison: pumpkin_checking::Comparison::NotEqual,
623 value: 2,
624 });
625 let state = VariableState::prepare_for_conflict_check(premises, consequent)
626 .expect("no conflicting atomics");
627
628 let checker = ElementChecker::new(vec!["x1", "x2"].into(), "x3", "x4");
629
630 assert!(checker.check(state, &premises, consequent.as_ref()));
631 }
632}