diff --git a/Cargo.lock b/Cargo.lock index 8c76ff9ed..5b4c30b54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,7 @@ name = "conjure_oxide" version = "0.0.1" dependencies = [ "json", + "minion_rs", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 399a556ec..06366bacd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,9 @@ members = [ "solvers/minion", "solvers/chuffed" ] + +[workspace.lints.clippy] +unwrap_used = "warn" +expect_used = "warn" + + diff --git a/conjure_oxide/Cargo.toml b/conjure_oxide/Cargo.toml index 8921daee8..1bb161df7 100644 --- a/conjure_oxide/Cargo.toml +++ b/conjure_oxide/Cargo.toml @@ -5,3 +5,8 @@ edition = "2021" [dependencies] json = "0.12.4" +minion_rs = {path = "../solvers/minion" } + +[lints] +workspace = true + diff --git a/conjure_oxide/src/ast.rs b/conjure_oxide/src/ast.rs index 90f583f13..c1eac5b61 100644 --- a/conjure_oxide/src/ast.rs +++ b/conjure_oxide/src/ast.rs @@ -39,10 +39,16 @@ pub enum Range { } #[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] pub enum Expression { ConstantInt(i32), Reference(Name), Sum(Vec), Eq(Box, Box), Geq(Box, Box), + + // Flattened Constraints + SumGeq(Vec, Box), + SumLeq(Vec, Box), + Ineq(Box, Box, Box), } diff --git a/conjure_oxide/src/lib.rs b/conjure_oxide/src/lib.rs index 851c0bc27..cec1c6424 100644 --- a/conjure_oxide/src/lib.rs +++ b/conjure_oxide/src/lib.rs @@ -1 +1,2 @@ pub mod ast; +mod solvers; diff --git a/conjure_oxide/src/main.rs b/conjure_oxide/src/main.rs index fe3c98315..4b843517e 100644 --- a/conjure_oxide/src/main.rs +++ b/conjure_oxide/src/main.rs @@ -31,7 +31,7 @@ fn main() { }, ); - let mut m = Model { + let m = Model { variables, constraints: vec![ Expression::Eq( diff --git a/conjure_oxide/src/solvers/minion.rs b/conjure_oxide/src/solvers/minion.rs new file mode 100644 index 000000000..e7b9fb529 --- /dev/null +++ b/conjure_oxide/src/solvers/minion.rs @@ -0,0 +1,276 @@ +//! Solver interface to minion_rs. + +use crate::ast::{ + DecisionVariable, Domain as ConjureDomain, Expression as ConjureExpression, + Model as ConjureModel, Name as ConjureName, Range as ConjureRange, +}; +use minion_rs::ast::{ + Constant as MinionConstant, Constraint as MinionConstraint, Model as MinionModel, + Var as MinionVar, VarDomain as MinionDomain, +}; + +impl TryFrom for MinionModel { + // TODO (nd60): set this to equal ConjureError once it is merged. + type Error = String; + + fn try_from(conjure_model: ConjureModel) -> Result { + let mut minion_model = MinionModel::new(); + + // We assume (for now) that the conjure model is fully valid + // i.e. type checked and the variables referenced all exist. + parse_vars(&conjure_model, &mut minion_model)?; + parse_exprs(&conjure_model, &mut minion_model)?; + + Ok(minion_model) + } +} + +fn parse_vars(conjure_model: &ConjureModel, minion_model: &mut MinionModel) -> Result<(), String> { + // TODO (nd60): remove unused vars? + // TODO (nd60): ensure all vars references are used. + + for (name, variable) in conjure_model.variables.iter() { + parse_var(name, variable, minion_model)?; + } + Ok(()) +} + +fn parse_var( + name: &ConjureName, + variable: &DecisionVariable, + minion_model: &mut MinionModel, +) -> Result<(), String> { + let str_name = name_to_string(name.to_owned()); + + let ranges = match &variable.domain { + ConjureDomain::IntDomain(range) => Ok(range), + x => Err(format!("Not supported: {:?}", x)), + }?; + + // TODO (nd60): Currently, Minion only supports the use of one range in the domain. + // If there are multiple ranges, SparseBound should be used here instead. + // See: https://github.com/conjure-cp/conjure-oxide/issues/84 + + if ranges.len() != 1 { + return Err(format!( + "Variable {:?} has {:?} ranges. Multiple ranges / SparseBound is not yet supported.", + str_name, + ranges.len() + )); + } + + let range = ranges + .first() + .ok_or(format!("Variable {:?} has no range", str_name))?; + + let (low, high) = match range { + ConjureRange::Bounded(x, y) => Ok((x.to_owned(), y.to_owned())), + ConjureRange::Single(x) => Ok((x.to_owned(), x.to_owned())), + a => Err(format!("Not implemented {:?}", a)), + }?; + + minion_model + .named_variables + .add_var(str_name.to_owned(), MinionDomain::Bound(low, high)) + .ok_or(format!("Variable {:?} is defined twice", str_name))?; + + Ok(()) +} + +fn parse_exprs(conjure_model: &ConjureModel, minion_model: &mut MinionModel) -> Result<(), String> { + for expr in conjure_model.constraints.iter() { + parse_expr(expr.to_owned(), minion_model)?; + } + Ok(()) +} + +fn parse_expr(expr: ConjureExpression, minion_model: &mut MinionModel) -> Result<(), String> { + match expr { + ConjureExpression::SumLeq(lhs, rhs) => parse_sumleq(lhs, *rhs, minion_model), + ConjureExpression::SumGeq(lhs, rhs) => parse_sumgeq(lhs, *rhs, minion_model), + ConjureExpression::Ineq(a, b, c) => parse_ineq(*a, *b, *c, minion_model), + x => Err(format!("Not supported: {:?}", x)), + } +} + +fn parse_sumleq( + sum_vars: Vec, + rhs: ConjureExpression, + minion_model: &mut MinionModel, +) -> Result<(), String> { + let minion_vars = must_be_vars(sum_vars)?; + let minion_rhs = must_be_var(rhs)?; + minion_model + .constraints + .push(MinionConstraint::SumLeq(minion_vars, minion_rhs)); + + Ok(()) +} + +fn parse_sumgeq( + sum_vars: Vec, + rhs: ConjureExpression, + minion_model: &mut MinionModel, +) -> Result<(), String> { + let minion_vars = must_be_vars(sum_vars)?; + let minion_rhs = must_be_var(rhs)?; + minion_model + .constraints + .push(MinionConstraint::SumGeq(minion_vars, minion_rhs)); + + Ok(()) +} + +fn parse_ineq( + a: ConjureExpression, + b: ConjureExpression, + c: ConjureExpression, + minion_model: &mut MinionModel, +) -> Result<(), String> { + let a_minion = must_be_var(a)?; + let b_minion = must_be_var(b)?; + let c_value = must_be_const(c)?; + minion_model.constraints.push(MinionConstraint::Ineq( + a_minion, + b_minion, + MinionConstant::Integer(c_value), + )); + + Ok(()) +} + +fn must_be_vars(exprs: Vec) -> Result, String> { + let mut minion_vars: Vec = vec![]; + for expr in exprs { + let minion_var = must_be_var(expr)?; + minion_vars.push(minion_var); + } + Ok(minion_vars) +} + +fn must_be_var(e: ConjureExpression) -> Result { + // a minion var is either a reference or a "var as const" + match must_be_ref(e.clone()) { + Ok(name) => Ok(MinionVar::NameRef(name)), + Err(_) => match must_be_const(e) { + Ok(n) => Ok(MinionVar::ConstantAsVar(n)), + Err(x) => Err(x), + }, + } +} + +fn must_be_ref(e: ConjureExpression) -> Result { + let name = match e { + ConjureExpression::Reference(n) => Ok(n), + _ => Err(""), + }?; + + let str_name = name_to_string(name); + Ok(str_name) +} + +fn must_be_const(e: ConjureExpression) -> Result { + match e { + ConjureExpression::ConstantInt(n) => Ok(n), + _ => Err("".to_owned()), + } +} + +fn name_to_string(name: ConjureName) -> String { + match name { + ConjureName::UserName(x) => x, + ConjureName::MachineName(x) => x.to_string(), + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use minion_rs::ast::VarName; + + use super::*; + + #[test] + fn flat_xyz_model() -> Result<(), String> { + // TODO: convert to use public interfaces when these exist. + let mut model = ConjureModel { + variables: HashMap::new(), + constraints: Vec::new(), + }; + + add_int_with_range(&mut model, "x", 1, 3)?; + add_int_with_range(&mut model, "y", 2, 4)?; + add_int_with_range(&mut model, "z", 1, 5)?; + + let x = ConjureExpression::Reference(ConjureName::UserName("x".to_owned())); + let y = ConjureExpression::Reference(ConjureName::UserName("y".to_owned())); + let z = ConjureExpression::Reference(ConjureName::UserName("z".to_owned())); + let four = ConjureExpression::ConstantInt(4); + + let geq = ConjureExpression::SumGeq( + vec![x.to_owned(), y.to_owned(), z.to_owned()], + Box::from(four.to_owned()), + ); + let leq = ConjureExpression::SumLeq( + vec![x.to_owned(), y.to_owned(), z.to_owned()], + Box::from(four.to_owned()), + ); + let ineq = ConjureExpression::Ineq(Box::from(x), Box::from(y), Box::from(four)); + + model.constraints.push(geq); + model.constraints.push(leq); + model.constraints.push(ineq); + + let minion_model = MinionModel::try_from(model)?; + minion_rs::run_minion(minion_model, xyz_callback).map_err(|x| x.to_string()) + } + + #[allow(clippy::unwrap_used)] + fn xyz_callback(solutions: HashMap) -> bool { + let x = match solutions.get("x").unwrap() { + MinionConstant::Integer(n) => n, + _ => panic!("x should be a integer"), + }; + + let y = match solutions.get("y").unwrap() { + MinionConstant::Integer(n) => n, + _ => panic!("y should be a integer"), + }; + + let z = match solutions.get("z").unwrap() { + MinionConstant::Integer(n) => n, + _ => panic!("z should be a integer"), + }; + + assert_eq!(*x, 1); + assert_eq!(*y, 2); + assert_eq!(*z, 1); + + false + } + + fn add_int_with_range( + model: &mut ConjureModel, + name: &str, + domain_low: i32, + domain_high: i32, + ) -> Result<(), String> { + // TODO: convert to use public interfaces when these exist. + let res = model.variables.insert( + ConjureName::UserName(name.to_owned()), + DecisionVariable { + domain: ConjureDomain::IntDomain(vec![ConjureRange::Bounded( + domain_low, + domain_high, + )]), + }, + ); + + match res { + // variable was not already present + None => Ok(()), + Some(_) => Err(format!("Variable {:?} was already present", name)), + } + } +} diff --git a/conjure_oxide/src/solvers/mod.rs b/conjure_oxide/src/solvers/mod.rs new file mode 100644 index 000000000..dad73cff9 --- /dev/null +++ b/conjure_oxide/src/solvers/mod.rs @@ -0,0 +1 @@ +pub mod minion; diff --git a/conjure_oxide/tests/model_tests.rs b/conjure_oxide/tests/model_tests.rs index edcf36890..0552362b9 100644 --- a/conjure_oxide/tests/model_tests.rs +++ b/conjure_oxide/tests/model_tests.rs @@ -18,9 +18,9 @@ fn modify_domain() { constraints: Vec::new(), }; - assert!((*m.variables.get(&a).unwrap()).domain == d1); + assert!(m.variables.get(&a).unwrap().domain == d1); m.update_domain(&a, d2.clone()); - assert!((*m.variables.get(&a).unwrap()).domain == d2); + assert!(m.variables.get(&a).unwrap().domain == d2); } diff --git a/solvers/chuffed/libwrapper.a b/solvers/chuffed/libwrapper.a deleted file mode 100644 index 2b9638b4f..000000000 Binary files a/solvers/chuffed/libwrapper.a and /dev/null differ diff --git a/solvers/chuffed/wrapper.o b/solvers/chuffed/wrapper.o deleted file mode 100644 index 5a7014984..000000000 Binary files a/solvers/chuffed/wrapper.o and /dev/null differ diff --git a/solvers/minion/Cargo.toml b/solvers/minion/Cargo.toml index f7ad03759..40a4b6dca 100644 --- a/solvers/minion/Cargo.toml +++ b/solvers/minion/Cargo.toml @@ -13,3 +13,7 @@ cc = { version = "1.0.84", features = ["parallel"] } bindgen = "0.69.1" glob = "0.3.1" + +[lints] +workspace = true + diff --git a/solvers/minion/build.rs b/solvers/minion/build.rs index 0df3d58a0..3479b8bff 100755 --- a/solvers/minion/build.rs +++ b/solvers/minion/build.rs @@ -2,7 +2,7 @@ // - https://github.com/gokberkkocak/rust_glucose/blob/master/build.rs // - https://rust-lang.github.io/rust-bindgen/non-system-libraries.html // - https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed - +// use std::env; use std::path::PathBuf; use std::process::Command;