Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the cohesion of the model when a view is part of unification #90

Merged
merged 1 commit into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions crates/fzn-huub/corpus/unify_with_view.fzn.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"variables": {
"x" : { "type" : "bool"},
"y" : { "type" : "bool"},
"z" : { "type" : "bool"},
"ivar" : { "type" : "int", "domain" : [[1, 3]]}
},
"arrays": {

},
"constraints": [
{ "id" : "bool_eq", "args" : ["x", "y"], "ann" : ["domain_change_constraint"]},
{ "id" : "bool_eq", "args" : ["x", "z"], "ann" : ["domain_change_constraint"]},
{ "id" : "int_le_reif", "args" : ["ivar", 2, "x"], "defines" : "x"}
],
"output": ["x", "y", "z", "ivar"],
"solve": { "method" : "satisfy"},
"version": "1.0"
}
16 changes: 16 additions & 0 deletions crates/fzn-huub/corpus/unify_with_view.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
x = false;
y = false;
z = false;
ivar = 3;
----------
x = true;
y = true;
z = true;
ivar = 1;
----------
x = true;
y = true;
z = true;
ivar = 2;
----------
==========
1 change: 1 addition & 0 deletions crates/fzn-huub/tests/flatzinc_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod tests {
assert_all_solutions!(unification);
assert_all_solutions!(unify_element_1);
assert_all_solutions!(unify_element_2);
assert_all_solutions!(unify_with_view);

assert_optimal!(jobshop);

Expand Down
285 changes: 163 additions & 122 deletions crates/huub/src/model/flatzinc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
cell::RefCell,
collections::BTreeMap,
collections::{btree_map::Entry, BTreeMap},
fmt::{Debug, Display},
ops::Deref,
rc::Rc,
Expand Down Expand Up @@ -35,8 +35,120 @@ impl Model {
let mut map = BTreeMap::<S, ModelView>::new();
let mut prb = Model::default();

// Unify variables (e.g., from `bool_eq` and `int_eq` constraints)
// Extract views from `defines_var` constraints
let mut processed = vec![false; fzn.constraints.len()];
for (i, c) in fzn.constraints.iter().enumerate() {
let mut add_view = |map: &mut BTreeMap<_, _>, name: S, view: ModelView| {
let e = map.insert(name, view);
debug_assert!(e.is_none());
processed[i] = true;
};

match (c.id.deref(), c.defines.as_ref()) {
("bool2int", Some(l)) => {
if let [b, Argument::Literal(Literal::Identifier(x))] = c.args.as_slice() {
if x == l {
let b = arg_bool(fzn, &mut prb, &mut map, b)?;
add_view(&mut map, l.clone(), IntView::from(b).into());
}
}
}
("bool_not", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Identifier(x)), b]
| [b, Argument::Literal(Literal::Identifier(x))]
if x == l =>
{
let b = arg_bool(fzn, &mut prb, &mut map, b)?;
add_view(&mut map, l.clone(), (!b).into());
}
_ => {}
},
("int_eq_reif", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Int(i)), Argument::Literal(x), Argument::Literal(Literal::Identifier(r))]
| [Argument::Literal(x), Argument::Literal(Literal::Int(i)), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
add_view(&mut map, l.clone(), BoolView::IntEq(Box::new(x), *i).into());
}
_ => {}
},
("int_le_reif", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Int(i)), Argument::Literal(x), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
add_view(
&mut map,
l.clone(),
BoolView::IntGreaterEq(Box::new(x), *i).into(),
);
}
[Argument::Literal(x), Argument::Literal(Literal::Int(i)), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
add_view(
&mut map,
l.clone(),
BoolView::IntLessEq(Box::new(x), *i).into(),
);
}
_ => {}
},
("int_ne_reif", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Int(i)), Argument::Literal(x), Argument::Literal(Literal::Identifier(r))]
| [Argument::Literal(x), Argument::Literal(Literal::Int(i)), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
add_view(
&mut map,
l.clone(),
BoolView::IntNotEq(Box::new(x), *i).into(),
);
}
_ => {}
},
("int_lin_eq", Some(l))
if c.args
.get(1)
.map(|v| arg_has_length(fzn, v, 2))
.unwrap_or(false) =>
'int_lin_eq: {
let [coeff, vars, sum] = c.args.as_slice() else {
break 'int_lin_eq;
};
let coeff = arg_array(fzn, coeff)?;
let vars = arg_array(fzn, vars)?;
let (c, (cy, vy)) = match vars.as_slice() {
[Literal::Identifier(v), y] if v == l => {
(par_int(fzn, &coeff[0])?, (par_int(fzn, &coeff[1])?, y))
}
[y, Literal::Identifier(v)] if v == l => {
(par_int(fzn, &coeff[1])?, (par_int(fzn, &coeff[0])?, y))
}
_ => break 'int_lin_eq,
};
let sum = arg_par_int(fzn, sum)?;
// c * l + cy * y = sum === l = (sum - cy * y) / c
if cy % c != 0 || sum % c != 0 {
break 'int_lin_eq;
}
let offset = sum / c;
let view = if let Some(scale) = NonZeroIntVal::new(-cy / c) {
let y = lit_int(fzn, &mut prb, &mut map, vy)?;
y * scale + offset
} else {
IntView::Const(offset)
};
add_view(&mut map, l.clone(), view.into());
}
_ => {}
}
}

// Unify variables (e.g., from `bool_eq` and `int_eq` constraints)
let mut unify_map = BTreeMap::<S, Rc<RefCell<Vec<Literal<S>>>>>::new();
let unify_map_find = |map: &BTreeMap<S, Rc<RefCell<Vec<Literal<S>>>>>, a: &Literal<S>| {
if let Literal::Identifier(x) = a {
Expand Down Expand Up @@ -124,7 +236,8 @@ impl Model {
}
let ty = &fzn.variables[&k].ty;
let li = li.borrow();
let var = match ty {
// Determine the domain of the list of literals
let domain: Option<Literal<S>> = match ty {
Type::Bool => {
let mut domain = None;
for lit in li.iter() {
Expand All @@ -140,10 +253,7 @@ impl Model {
_ => unreachable!(),
};
}
match domain {
Some(b) => ModelView::Bool(BoolView::Const(b)),
None => ModelView::Bool(prb.new_bool_var()),
}
domain.map(Literal::Bool)
}
Type::Int => {
let mut domain = None::<RangeList<IntVal>>;
Expand All @@ -169,129 +279,60 @@ impl Model {
_ => unreachable!(),
};
}
if let Some(domain) = domain {
if domain.is_empty() {
return Err(ReformulationError::TrivialUnsatisfiable.into());
} else if domain.card() == 1 {
ModelView::Int(IntView::Const(*domain.lower_bound().unwrap()))
} else {
ModelView::Int(prb.new_int_var(domain))
}
} else {
todo!("Variables without a domain are not yet supported")
}
domain.map(Literal::IntSet)
}
_ => unreachable!(),
};
// Find any view that is part of a unified group
let var = li
.iter()
.find_map(|lit| -> Option<ModelView> {
if let Literal::Identifier(id) = lit {
map.get(id).cloned()
} else {
None
}
})
// Create a new variable if no view is found
.unwrap_or_else(|| match domain {
Some(Literal::Bool(b)) => BoolView::Const(b).into(),
Some(Literal::IntSet(dom)) => prb.new_int_var(dom).into(),
Some(_) => unreachable!(),
None => match ty {
Type::Bool => prb.new_bool_var().into(),
Type::Int => panic!("unbounded integer variables are not supported yet"),
_ => unreachable!(),
},
});

// Map (or equate) all names in the group to the new variable
for lit in li.iter() {
if let Literal::Identifier(id) = lit {
let x = map.insert(id.clone(), var.clone());
debug_assert!(x.is_none());
}
}
}

// Extract views from `defines_var` constraints
for (i, c) in fzn.constraints.iter().enumerate() {
let mut mark_processed = || processed[i] = true;

match (c.id.deref(), c.defines.as_ref()) {
("bool2int", Some(l)) => {
if let [b, Argument::Literal(Literal::Identifier(x))] = c.args.as_slice() {
if x == l {
let b = arg_bool(fzn, &mut prb, &mut map, b)?;
let _ = map.insert(l.clone(), IntView::from(b).into());
mark_processed();
}
}
}
("bool_not", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Identifier(x)), b]
| [b, Argument::Literal(Literal::Identifier(x))]
if x == l =>
{
let b = arg_bool(fzn, &mut prb, &mut map, b)?;
let _ = map.insert(l.clone(), (!b).into());
mark_processed();
}
_ => {}
},
("int_eq_reif", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Int(i)), Argument::Literal(x), Argument::Literal(Literal::Identifier(r))]
| [Argument::Literal(x), Argument::Literal(Literal::Int(i)), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
let _ = map.insert(l.clone(), BoolView::IntEq(Box::new(x), *i).into());
mark_processed();
}
_ => {}
},
("int_le_reif", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Int(i)), Argument::Literal(x), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
let _ =
map.insert(l.clone(), BoolView::IntGreaterEq(Box::new(x), *i).into());
mark_processed();
}
[Argument::Literal(x), Argument::Literal(Literal::Int(i)), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
let _ = map.insert(l.clone(), BoolView::IntLessEq(Box::new(x), *i).into());
mark_processed();
}
_ => {}
},
("int_ne_reif", Some(l)) => match c.args.as_slice() {
[Argument::Literal(Literal::Int(i)), Argument::Literal(x), Argument::Literal(Literal::Identifier(r))]
| [Argument::Literal(x), Argument::Literal(Literal::Int(i)), Argument::Literal(Literal::Identifier(r))]
if r == l =>
{
let x = lit_int(fzn, &mut prb, &mut map, x)?;
let _ = map.insert(l.clone(), BoolView::IntNotEq(Box::new(x), *i).into());
mark_processed();
}
_ => {}
},
("int_lin_eq", Some(l))
if c.args
.get(1)
.map(|v| arg_has_length(fzn, v, 2))
.unwrap_or(false) =>
'int_lin_eq: {
let [coeff, vars, sum] = c.args.as_slice() else {
break 'int_lin_eq;
};
let coeff = arg_array(fzn, coeff)?;
let vars = arg_array(fzn, vars)?;
let (c, (cy, vy)) = match vars.as_slice() {
[Literal::Identifier(v), y] if v == l => {
(par_int(fzn, &coeff[0])?, (par_int(fzn, &coeff[1])?, y))
}
[y, Literal::Identifier(v)] if v == l => {
(par_int(fzn, &coeff[1])?, (par_int(fzn, &coeff[0])?, y))
match map.entry(id.clone()) {
Entry::Vacant(e) => {
let _ = e.insert(var.clone());
}
_ => break 'int_lin_eq,
};
let sum = arg_par_int(fzn, sum)?;
// c * l + cy * y = sum === l = (sum - cy * y) / c
if cy % c != 0 || sum % c != 0 {
break 'int_lin_eq;
Entry::Occupied(e) => match ty {
Type::Bool => {
let (ModelView::Bool(new), ModelView::Bool(existing)) =
(var.clone(), e.get().clone())
else {
unreachable!()
};
prb += BoolExpr::Equiv(vec![new.into(), existing.into()])
}
Type::Int => {
let (ModelView::Int(new), ModelView::Int(existing)) =
(var.clone(), e.get().clone())
else {
unreachable!()
};
prb += Constraint::IntLinEq(vec![new, existing * -1], 0)
}
_ => unreachable!(),
},
}
let offset = sum / c;
let view = if let Some(scale) = NonZeroIntVal::new(-cy / c) {
let y = lit_int(fzn, &mut prb, &mut map, vy)?;
y * scale + offset
} else {
IntView::Const(offset)
};
let _ = map.insert(l.clone(), view.into());
mark_processed();
}
_ => {}
}
}

Expand Down
Loading