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