Skip to content

Commit 8f43208

Browse files
feat: Post equality as single predicate instead of splitting in two (#305)
Currently, whenever we post a predicate `[x == v]`, we decompose it into `[x >= v] /\ [x <= v]`. However, this can make certain structures unnecessarily complicated. This PR aims to address this by posting `[x == v]` directly instead of decomposing. ## TODO - [x] Check for any other locations where we make this assumption (@maartenflippo and @EmirDe, your input would be much appreciated here!) - [x] Check the performance ## Experiments ### Overall `main`: ``` {'ERROR': 19, 'OPTIMAL': 120, 'SATISFIABLE': 228, 'UNKNOWN': 114, 'UNSATISFIABLE': 6} ``` `feat/post-equality-as-single-predicate`: ``` {'ERROR': 19, 'OPTIMAL': 122, 'SATISFIABLE': 228, 'UNKNOWN': 112, 'UNSATISFIABLE': 6} ``` ### MiniZinc Scoring ``` { 'main': 180.59, 'feat/post-equality-as-single-predicate': 177.41 } ``` ### Primal Integral ``` { 'main': 25.06, 'feat/post-equality-as-single-predicate': 23.63, } ```
1 parent a3fb929 commit 8f43208

File tree

5 files changed

+107
-198
lines changed

5 files changed

+107
-198
lines changed

pumpkin-crates/core/src/basic_types/predicate_id_generators/predicate_id_generator.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use crate::containers::HashMap;
22
use crate::containers::KeyedVec;
33
use crate::containers::StorageKey;
44
use crate::engine::predicates::predicate::Predicate;
5-
use crate::pumpkin_assert_moderate;
65

76
#[derive(Debug, Default, Clone)]
87
pub(crate) struct PredicateIdGenerator {
@@ -11,10 +10,6 @@ pub(crate) struct PredicateIdGenerator {
1110
}
1211

1312
impl PredicateIdGenerator {
14-
pub(crate) fn has_id_for_predicate(&self, predicate: Predicate) -> bool {
15-
self.predicate_to_id.contains_key(&predicate)
16-
}
17-
1813
/// Returns an id for the predicate. If the predicate already has an id, its id is returned.
1914
/// Otherwise, a new id is create and returned.
2015
pub(crate) fn get_id(&mut self, predicate: Predicate) -> PredicateId {
@@ -36,12 +31,6 @@ impl PredicateIdGenerator {
3631
self.predicate_to_id.clear();
3732
}
3833

39-
pub(crate) fn replace_predicate(&mut self, predicate: Predicate, replacement: Predicate) {
40-
pumpkin_assert_moderate!(self.has_id_for_predicate(predicate));
41-
let predicate_id = self.get_id(predicate);
42-
self.id_to_predicate[predicate_id] = replacement
43-
}
44-
4534
pub(crate) fn num_predicate_ids(&self) -> usize {
4635
self.id_to_predicate.len()
4736
}

pumpkin-crates/core/src/engine/conflict_analysis/conflict_analysis_context.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,16 @@ impl ConflictAnalysisContext<'_> {
472472
reason_buffer.extend(std::iter::once(predicate_lb));
473473
reason_buffer.extend(std::iter::once(predicate_ub));
474474
}
475+
(
476+
PredicateType::Equal,
477+
PredicateType::LowerBound | PredicateType::UpperBound | PredicateType::NotEqual,
478+
) => {
479+
// The trail predicate is equality, but the input predicate is either a
480+
// lower-bound, upper-bound, or not equals.
481+
//
482+
// TODO: could consider lifting here
483+
reason_buffer.extend(std::iter::once(trail_entry.predicate))
484+
}
475485
_ => unreachable!(
476486
"Unreachable combination of {} and {}",
477487
trail_entry.predicate, predicate

pumpkin-crates/core/src/engine/conflict_analysis/resolvers/resolution_resolver.rs

Lines changed: 1 addition & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -130,125 +130,7 @@ impl ConflictResolver for ResolutionResolver {
130130
// 1) Pop the predicate last assigned on the trail from the nogood.
131131
let next_predicate = self.pop_predicate_from_conflict_nogood();
132132

133-
// 2) Add the reason of the next_predicate to the nogood.
134-
135-
// 2.a) Here we treat the special case: if the next predicate is a decision, this means
136-
// that we are done with analysis since the only remaining predicate in the heap is the
137-
// other decision.
138-
if context.assignments.is_decision_predicate(&next_predicate) {
139-
// As a simple workaround, we add the currently analysed predicate to set of
140-
// predicates from the lower predicate level, and stop analysis.
141-
//
142-
// Semantic minimisation will ensure the bound predicates get converted into an
143-
// equality decision predicate.
144-
while self.to_process_heap.num_nonremoved_elements() != 1 {
145-
let predicate = self.pop_predicate_from_conflict_nogood();
146-
let predicate_replacement = if context
147-
.assignments
148-
.is_decision_predicate(&predicate)
149-
{
150-
predicate
151-
} else {
152-
// Note that we decompose [x == v] into the two predicates [x >= v] and [x
153-
// <= v] and that these have distinct trail entries (where [x >= v] has a
154-
// lower trail position than [x <= v])
155-
//
156-
// However, this can lead to [x <= v] to be processed *before* [x >= v -
157-
// y], meaning that these implied predicates should be replaced with their
158-
// reason
159-
self.reason_buffer.clear();
160-
ConflictAnalysisContext::get_propagation_reason(
161-
predicate,
162-
context.assignments,
163-
CurrentNogood::new(
164-
&self.to_process_heap,
165-
&self.processed_nogood_predicates,
166-
&self.predicate_id_generator,
167-
),
168-
context.reason_store,
169-
context.propagators,
170-
context.proof_log,
171-
context.unit_nogood_inference_codes,
172-
&mut self.reason_buffer,
173-
context.notification_engine,
174-
context.variable_names,
175-
);
176-
177-
if self.reason_buffer.is_empty() {
178-
predicate
179-
} else {
180-
pumpkin_assert_simple!(predicate.is_lower_bound_predicate() || predicate.is_not_equal_predicate(), "A non-decision predicate in the nogood should be either a lower-bound or a not-equals predicate but it was {predicate} with reason {:?}", self.reason_buffer);
181-
pumpkin_assert_simple!(
182-
self.reason_buffer.len() == 1 && self.reason_buffer[0].is_lower_bound_predicate(),
183-
"The reason for the only propagated predicates left on the trail should be lower-bound predicates, but the reason for {predicate} was {:?}",
184-
self.reason_buffer,
185-
);
186-
187-
self.reason_buffer[0]
188-
}
189-
};
190-
191-
// We push to `predicates_lower_decision_level` since this structure will be
192-
// used for creating the final nogood
193-
self.processed_nogood_predicates.push(predicate_replacement);
194-
}
195-
// It could be the case that the final predicate in the nogood is implied, in
196-
// this case we eagerly replace it since the conflict analysis output assumes
197-
// that a single variable is propagating.
198-
//
199-
// For example, let's say we have made the decision [x == v] (which is
200-
// decomposed into [x >= v] and [x <= v]).
201-
//
202-
// Now let's say we have the nogood [[x>= v - 1], [x <= v]], then we have a
203-
// final element [x >= v - 1] left in the heap which is
204-
// implied. This could mean that we end up with 2 predicates in
205-
// the conflict nogood which goes against the 2-watcher scheme so we eagerly
206-
// replace it here!
207-
//
208-
// If it is an initial bound then it will be removed by semantic minimisation when
209-
// extracting the final nogood.
210-
//
211-
// TODO: This leads to a less general explanation!
212-
if !context
213-
.assignments
214-
.is_decision_predicate(&self.peek_predicate_from_conflict_nogood())
215-
&& !context
216-
.assignments
217-
.is_initial_bound(self.peek_predicate_from_conflict_nogood())
218-
{
219-
let predicate = self.peek_predicate_from_conflict_nogood();
220-
221-
self.reason_buffer.clear();
222-
ConflictAnalysisContext::get_propagation_reason(
223-
predicate,
224-
context.assignments,
225-
CurrentNogood::new(
226-
&self.to_process_heap,
227-
&self.processed_nogood_predicates,
228-
&self.predicate_id_generator,
229-
),
230-
context.reason_store,
231-
context.propagators,
232-
context.proof_log,
233-
context.unit_nogood_inference_codes,
234-
&mut self.reason_buffer,
235-
context.notification_engine,
236-
context.variable_names,
237-
);
238-
pumpkin_assert_simple!(predicate.is_lower_bound_predicate() || predicate.is_not_equal_predicate() , "If the final predicate in the conflict nogood is not a decision predicate then it should be either a lower-bound predicate or a not-equals predicate but was {predicate}");
239-
pumpkin_assert_simple!(
240-
self.reason_buffer.len() == 1 && self.reason_buffer[0].is_lower_bound_predicate(),
241-
"The reason for the decision predicate should be a lower-bound predicate but was {}", self.reason_buffer[0]
242-
);
243-
self.replace_predicate_in_conflict_nogood(predicate, self.reason_buffer[0]);
244-
}
245-
246-
// The final predicate in the heap will get pushed in `extract_final_nogood`
247-
self.processed_nogood_predicates.push(next_predicate);
248-
break;
249-
}
250-
251-
// 2.b) Standard case, get the reason for the predicate and add it to the nogood.
133+
// 2) Get the reason for the predicate and add it to the nogood.
252134
self.reason_buffer.clear();
253135
ConflictAnalysisContext::get_propagation_reason(
254136
next_predicate,
@@ -428,21 +310,6 @@ impl ResolutionResolver {
428310
self.predicate_id_generator.get_predicate(next_predicate_id)
429311
}
430312

431-
fn peek_predicate_from_conflict_nogood(&self) -> Predicate {
432-
let next_predicate_id = self.to_process_heap.peek_max().unwrap().0;
433-
self.predicate_id_generator
434-
.get_predicate(*next_predicate_id)
435-
}
436-
437-
fn replace_predicate_in_conflict_nogood(
438-
&mut self,
439-
predicate: Predicate,
440-
replacement: Predicate,
441-
) {
442-
self.predicate_id_generator
443-
.replace_predicate(predicate, replacement);
444-
}
445-
446313
fn extract_final_nogood(&mut self, context: &mut ConflictAnalysisContext) -> LearnedNogood {
447314
// The final nogood is composed of the predicates encountered from the lower decision
448315
// levels, plus the predicate remaining in the heap.

pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,7 +1094,7 @@ impl ConstraintSatisfactionSolver {
10941094
empty_domain_reason.push(predicate!(conflict_domain == entry.old_lower_bound));
10951095
}
10961096
PredicateType::Equal => {
1097-
// The last trail entry was an equality propagation; we split into two cases.
1097+
// The last trail entry was an equality propagation; we split into three cases.
10981098
if entry.predicate.get_right_hand_side() < entry.old_lower_bound {
10991099
// 1) The assigned value was lower than the lower-bound
11001100
//
@@ -1103,17 +1103,19 @@ impl ConstraintSatisfactionSolver {
11031103
empty_domain_reason.push(predicate!(
11041104
conflict_domain >= entry.predicate.get_right_hand_side() + 1
11051105
));
1106-
} else {
1106+
} else if entry.predicate.get_right_hand_side() > entry.old_upper_bound {
11071107
// 2) The assigned value was larger than the upper-bound
11081108
//
11091109
// We lift so that it is the most general upper-bound possible while still
11101110
// causing the empty domain
1111-
pumpkin_assert_simple!(
1112-
entry.predicate.get_right_hand_side() > entry.old_upper_bound
1113-
);
11141111
empty_domain_reason.push(predicate!(
11151112
conflict_domain <= entry.predicate.get_right_hand_side() - 1
11161113
));
1114+
} else {
1115+
// 3) The assigned value was equal to a hole in the domain
1116+
empty_domain_reason.push(predicate!(
1117+
conflict_domain != entry.predicate.get_right_hand_side()
1118+
))
11171119
}
11181120
}
11191121
}

pumpkin-crates/core/src/engine/cp/assignments.rs

Lines changed: 89 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -518,14 +518,41 @@ impl Assignments {
518518
reason: Option<AssignmentReason>,
519519
) -> Result<bool, EmptyDomain> {
520520
let mut update_took_place = false;
521-
// only tighten the lower bound if needed
522-
if self.get_lower_bound(domain_id) < assigned_value {
523-
update_took_place |= self.tighten_lower_bound(domain_id, assigned_value, reason)?;
521+
522+
let predicate = predicate!(domain_id == assigned_value);
523+
524+
let old_lower_bound = self.get_lower_bound(domain_id);
525+
let old_upper_bound = self.get_upper_bound(domain_id);
526+
527+
if old_lower_bound == assigned_value && old_upper_bound == assigned_value {
528+
return self.domains[domain_id].verify_consistency();
524529
}
525530

526-
// only tighten the upper bound if needed
527-
if self.get_upper_bound(domain_id) > assigned_value {
528-
update_took_place |= self.tighten_upper_bound(domain_id, assigned_value, reason)?;
531+
// important to record trail position _before_ pushing to the trail
532+
let trail_position = self.trail.len();
533+
534+
self.trail.push(ConstraintProgrammingTrailEntry {
535+
predicate,
536+
old_lower_bound,
537+
old_upper_bound,
538+
reason,
539+
});
540+
541+
let decision_level = self.get_decision_level();
542+
let domain = &mut self.domains[domain_id];
543+
544+
if old_lower_bound < assigned_value {
545+
update_took_place |=
546+
domain.set_lower_bound(assigned_value, decision_level, trail_position);
547+
self.bounds[domain_id].0 = domain.lower_bound();
548+
self.pruned_values += domain.lower_bound().abs_diff(old_lower_bound) as u64;
549+
}
550+
551+
if old_upper_bound > assigned_value {
552+
update_took_place |=
553+
domain.set_upper_bound(assigned_value, decision_level, trail_position);
554+
self.bounds[domain_id].1 = domain.upper_bound();
555+
self.pruned_values += domain.upper_bound().abs_diff(old_upper_bound) as u64;
529556
}
530557

531558
let _ = self.domains[domain_id].verify_consistency()?;
@@ -702,43 +729,50 @@ impl Assignments {
702729
self.trail.get_decision_level(),
703730
);
704731

705-
self.trail.synchronise(new_decision_level).enumerate().for_each(|(index, entry)| {
706-
pumpkin_assert_moderate!(
707-
!entry.predicate.is_equality_predicate(),
708-
"For now we do not expect equality predicates on the trail, since currently equality predicates are split into lower and upper bound predicates."
709-
);
710-
711-
// Calculate how many values are re-introduced into the domain.
712-
let domain_id = entry.predicate.get_domain();
713-
let lower_bound_before = self.domains[domain_id].lower_bound();
714-
let upper_bound_before = self.domains[domain_id].upper_bound();
715-
716-
let trail_index = num_trail_entries_before_synchronisation - index - 1;
717-
718-
let add_on_upper_bound = entry.old_upper_bound.abs_diff(upper_bound_before) as u64;
719-
let add_on_lower_bound = lower_bound_before.abs_diff(entry.old_lower_bound) as u64;
720-
self.pruned_values -= add_on_upper_bound + add_on_lower_bound;
721-
722-
if entry.predicate.is_not_equal_predicate() && add_on_lower_bound + add_on_upper_bound == 0 {
723-
self.pruned_values -= 1;
724-
}
725-
726-
let fixed_before = self.domains[domain_id].lower_bound() == self.domains[domain_id].upper_bound();
727-
self.domains[domain_id].undo_trail_entry(&entry);
728-
729-
let new_lower_bound = self.domains[domain_id].lower_bound();
730-
let new_upper_bound = self.domains[domain_id].upper_bound();
731-
self.bounds[domain_id] = (new_lower_bound, new_upper_bound);
732-
733-
notification_engine.undo_trail_entry(fixed_before, lower_bound_before, upper_bound_before, new_lower_bound, new_upper_bound, trail_index, entry.predicate);
734-
735-
if new_lower_bound != new_upper_bound {
736-
737-
// Variable used to be fixed but is not after backtracking
738-
unfixed_variables.push((domain_id, lower_bound_before));
739-
}
732+
self.trail
733+
.synchronise(new_decision_level)
734+
.enumerate()
735+
.for_each(|(index, entry)| {
736+
// Calculate how many values are re-introduced into the domain.
737+
let domain_id = entry.predicate.get_domain();
738+
let lower_bound_before = self.domains[domain_id].lower_bound();
739+
let upper_bound_before = self.domains[domain_id].upper_bound();
740+
741+
let trail_index = num_trail_entries_before_synchronisation - index - 1;
742+
743+
let add_on_upper_bound = entry.old_upper_bound.abs_diff(upper_bound_before) as u64;
744+
let add_on_lower_bound = entry.old_lower_bound.abs_diff(lower_bound_before) as u64;
745+
self.pruned_values -= add_on_upper_bound + add_on_lower_bound;
746+
747+
if entry.predicate.is_not_equal_predicate()
748+
&& add_on_lower_bound + add_on_upper_bound == 0
749+
{
750+
self.pruned_values -= 1;
751+
}
740752

741-
});
753+
let fixed_before =
754+
self.domains[domain_id].lower_bound() == self.domains[domain_id].upper_bound();
755+
self.domains[domain_id].undo_trail_entry(&entry);
756+
757+
let new_lower_bound = self.domains[domain_id].lower_bound();
758+
let new_upper_bound = self.domains[domain_id].upper_bound();
759+
self.bounds[domain_id] = (new_lower_bound, new_upper_bound);
760+
761+
notification_engine.undo_trail_entry(
762+
fixed_before,
763+
lower_bound_before,
764+
upper_bound_before,
765+
new_lower_bound,
766+
new_upper_bound,
767+
trail_index,
768+
entry.predicate,
769+
);
770+
771+
if new_lower_bound != new_upper_bound {
772+
// Variable used to be fixed but is not after backtracking
773+
unfixed_variables.push((domain_id, lower_bound_before));
774+
}
775+
});
742776

743777
// Drain does not remove the events from the internal data structure. Elements are removed
744778
// lazily, as the iterator gets executed. For this reason we go through the entire iterator.
@@ -1187,17 +1221,24 @@ impl IntegerDomain {
11871221
}
11881222
}
11891223
PredicateType::Equal => {
1190-
// I think we never push equality predicates to the trail
1191-
// in the current version. Equality gets substituted
1192-
// by a lower and upper bound predicate.
1193-
unreachable!()
1224+
let lower_bound_update = self.lower_bound_updates.last().unwrap();
1225+
let upper_bound_update = self.upper_bound_updates.last().unwrap();
1226+
1227+
if lower_bound_update.trail_position > upper_bound_update.trail_position {
1228+
let _ = self.lower_bound_updates.pop();
1229+
} else if upper_bound_update.trail_position > lower_bound_update.trail_position {
1230+
let _ = self.upper_bound_updates.pop();
1231+
} else {
1232+
let _ = self.lower_bound_updates.pop();
1233+
let _ = self.upper_bound_updates.pop();
1234+
}
11941235
}
11951236
};
11961237

11971238
// these asserts will be removed, for now it is a sanity check
11981239
// later we may remove the old bound from the trail entry since it is not needed
1199-
pumpkin_assert_simple!(self.lower_bound() == entry.old_lower_bound);
1200-
pumpkin_assert_simple!(self.upper_bound() == entry.old_upper_bound);
1240+
pumpkin_assert_eq_simple!(self.lower_bound(), entry.old_lower_bound);
1241+
pumpkin_assert_eq_simple!(self.upper_bound(), entry.old_upper_bound);
12011242

12021243
pumpkin_assert_moderate!(self.debug_bounds_check());
12031244
}

0 commit comments

Comments
 (0)