Skip to content

Commit 35a91b5

Browse files
authored
Merge pull request #136 from plaans/fix/135
fix(sat): derived clause were incorretly too strong in some corner cases
2 parents 98e9d5d + 9feca11 commit 35a91b5

File tree

3 files changed

+92
-13
lines changed

3 files changed

+92
-13
lines changed

ci/scheduling.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
solver_cmd = solver + " {kind} {instance} --expected-makespan {makespan}"
1313

1414
instances = [
15-
("jobshop", "examples/scheduling/instances/jobshop/ft06.jsp", 55),
16-
("jobshop", "examples/scheduling/instances/jobshop/la01.jsp", 666),
17-
("openshop", "examples/scheduling/instances/openshop/taillard/tai04_04_01.osp", 193),
18-
]
15+
("jobshop", "examples/scheduling/instances/jobshop/ft06.jsp", 55),
16+
("jobshop", "examples/scheduling/instances/jobshop/la01.jsp", 666),
17+
("openshop", "examples/scheduling/instances/openshop/taillard/tai04_04_01.osp", 193),
18+
] + [
19+
("jobshop", "examples/scheduling/instances/jobshop/orb05.jsp", 887),
20+
] * 30
1921

2022
for (kind, instance, makespan) in instances:
2123
cmd = solver_cmd.format(kind=kind, instance=instance, makespan=makespan).split(" ")
@@ -24,5 +26,3 @@
2426
if solver_run.returncode != 0:
2527
print("Solver did not return expected result")
2628
exit(1)
27-
28-

solver/src/core/literals/disjunction.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::core::*;
2+
use itertools::Itertools;
23
use std::borrow::Borrow;
4+
use std::collections::HashMap;
35
use std::fmt::{Debug, Formatter};
46

57
/// A set of literals representing a disjunction.
@@ -145,9 +147,57 @@ impl AsRef<[Lit]> for Disjunction {
145147
}
146148
}
147149

150+
/// A builder for a disjunction that avoids duplicated literals
151+
#[derive(Default, Clone)]
152+
pub struct DisjunctionBuilder {
153+
upper_bounds: HashMap<SignedVar, IntCst>,
154+
}
155+
156+
impl DisjunctionBuilder {
157+
pub fn new() -> Self {
158+
Default::default()
159+
}
160+
161+
pub fn with_capacity(n: usize) -> Self {
162+
Self {
163+
upper_bounds: HashMap::with_capacity(n),
164+
}
165+
}
166+
167+
pub fn is_empty(&self) -> bool {
168+
self.upper_bounds.is_empty()
169+
}
170+
171+
pub fn push(&mut self, lit: Lit) {
172+
let sv = lit.svar();
173+
let ub = lit.bound_value().as_int();
174+
let new_ub = if let Some(prev) = self.upper_bounds.get(&sv) {
175+
// (sv <= ub) || (sv <= prev) <=> (sv <= max(ub, prev))
176+
ub.max(*prev)
177+
} else {
178+
ub
179+
};
180+
self.upper_bounds.insert(sv, new_ub);
181+
}
182+
183+
pub fn literals(&self) -> impl Iterator<Item = Lit> + '_ {
184+
self.upper_bounds
185+
.iter()
186+
.map(|(k, v)| Lit::from_parts(*k, UpperBound::ub(*v)))
187+
}
188+
}
189+
190+
impl From<DisjunctionBuilder> for Disjunction {
191+
fn from(value: DisjunctionBuilder) -> Self {
192+
Disjunction::new(value.literals().collect_vec())
193+
}
194+
}
195+
148196
#[cfg(test)]
149197
mod tests {
150198
use super::*;
199+
use rand::seq::SliceRandom;
200+
use rand::thread_rng;
151201

152202
fn leq(var: VarRef, val: IntCst) -> Lit {
153203
Lit::leq(var, val)
@@ -213,4 +263,33 @@ mod tests {
213263
assert!(Disjunction::new(vec![leq(a, 0), geq(a, 1)]).is_tautology());
214264
assert!(Disjunction::new(vec![leq(a, 0), leq(b, 0), geq(b, 2), !leq(a, 0)]).is_tautology());
215265
}
266+
267+
#[test]
268+
fn test_builder() {
269+
let vars = (0..10).map(VarRef::from_u32);
270+
271+
let vals = 0..10;
272+
273+
// create a large set of literals from which to generate disjunction
274+
let mut lits = Vec::new();
275+
for var in vars {
276+
for val in vals.clone() {
277+
lits.push(Lit::geq(var, val));
278+
lits.push(Lit::leq(var, val));
279+
}
280+
}
281+
282+
// select many subsets of the literals and test if going through the builder yields the correct output
283+
for _ in 0..100 {
284+
lits.shuffle(&mut thread_rng());
285+
let subset = &lits[0..30];
286+
let disj = Disjunction::new(subset.to_vec());
287+
let mut builder = DisjunctionBuilder::new();
288+
for l in subset {
289+
builder.push(*l);
290+
}
291+
let built: Disjunction = builder.into();
292+
assert_eq!(disj, built);
293+
}
294+
}
216295
}

solver/src/core/state/domains.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::backtrack::{Backtrack, DecLvl, DecisionLevelClass, EventIndex, ObsTrail};
22
use crate::collections::ref_store::RefMap;
3-
use crate::core::literals::{Disjunction, ImplicationGraph, LitSet};
3+
use crate::core::literals::{Disjunction, DisjunctionBuilder, ImplicationGraph, LitSet};
44
use crate::core::state::cause::{DirectOrigin, Origin};
55
use crate::core::state::event::Event;
66
use crate::core::state::int_domains::IntDomains;
@@ -446,7 +446,7 @@ impl Domains {
446446
// literals falsified at the current decision level, we need to proceed until there is a single one left (1UIP)
447447
self.queue.clear();
448448
// literals that are beyond the current decision level and will be part of the final clause
449-
let mut result: LitSet = LitSet::with_capacity(128);
449+
let mut result: DisjunctionBuilder = DisjunctionBuilder::with_capacity(32);
450450

451451
let decision_level = self.current_decision_level();
452452
let mut resolved = LitSet::new();
@@ -466,15 +466,15 @@ impl Domains {
466466
}
467467
DecisionLevelClass::Intermediate => {
468468
// implied before the current decision level, the negation of the literal will appear in the final clause (1UIP)
469-
result.insert(!l)
469+
result.push(!l)
470470
}
471471
}
472472
}
473473
} else {
474474
// the event is not entailed, must be part of an eager propagation
475475
// Even if it was not necessary for this propagation to occur, it must be part of
476476
// the clause for correctness
477-
result.insert(!l)
477+
result.push(!l)
478478
}
479479
}
480480
debug_assert!(explanation.lits.is_empty());
@@ -487,7 +487,7 @@ impl Domains {
487487
// if we were at the root decision level, we should have derived the empty clause
488488
debug_assert!(decision_level != DecLvl::ROOT || result.is_empty());
489489
return Conflict {
490-
clause: Disjunction::new(result.into()),
490+
clause: result.into(),
491491
resolved,
492492
};
493493
}
@@ -526,9 +526,9 @@ impl Domains {
526526
// the content of result is a conjunction of literal that imply `!l`
527527
// build the conflict clause and exit
528528
debug_assert!(self.queue.is_empty());
529-
result.insert(!l.lit);
529+
result.push(!l.lit);
530530
return Conflict {
531-
clause: Disjunction::new(result.into()),
531+
clause: result.into(),
532532
resolved,
533533
};
534534
}

0 commit comments

Comments
 (0)