1
//! Solver interface to minion_rs.
2

            
3
use super::{FromConjureModel, SolverError};
4
use crate::Solver;
5

            
6
use crate::ast::{
7
    Constant as ConjureConstant, DecisionVariable, Domain as ConjureDomain,
8
    Expression as ConjureExpression, Model as ConjureModel, Name as ConjureName,
9
    Range as ConjureRange,
10
};
11
pub use minion_rs::ast::Model as MinionModel;
12
use minion_rs::ast::{
13
    Constant as MinionConstant, Constraint as MinionConstraint, Var as MinionVar,
14
    VarDomain as MinionDomain,
15
};
16

            
17
const SOLVER: Solver = Solver::Minion;
18

            
19
impl FromConjureModel for MinionModel {
20
1
    fn from_conjure(conjure_model: ConjureModel) -> Result<Self, SolverError> {
21
1
        let mut minion_model = MinionModel::new();
22
1

            
23
1
        // We assume (for now) that the conjure model is fully valid
24
1
        // i.e. type checked and the variables referenced all exist.
25
1
        parse_vars(&conjure_model, &mut minion_model)?;
26
1
        parse_exprs(&conjure_model, &mut minion_model)?;
27

            
28
1
        Ok(minion_model)
29
1
    }
30
}
31

            
32
1
fn parse_vars(
33
1
    conjure_model: &ConjureModel,
34
1
    minion_model: &mut MinionModel,
35
1
) -> Result<(), SolverError> {
36
    // TODO (nd60): remove unused vars?
37
    // TODO (nd60): ensure all vars references are used.
38

            
39
3
    for (name, variable) in conjure_model.variables.iter() {
40
3
        parse_var(name, variable, minion_model)?;
41
    }
42
1
    Ok(())
43
1
}
44

            
45
3
fn parse_var(
46
3
    name: &ConjureName,
47
3
    variable: &DecisionVariable,
48
3
    minion_model: &mut MinionModel,
49
3
) -> Result<(), SolverError> {
50
3
    let str_name = name_to_string(name.to_owned());
51

            
52
3
    let ranges = match &variable.domain {
53
3
        ConjureDomain::IntDomain(range) => Ok(range),
54
        x => Err(SolverError::NotSupported(SOLVER, format!("{:?}", x))),
55
    }?;
56

            
57
    // TODO (nd60): Currently, Minion only supports the use of one range in the domain.
58
    // If there are multiple ranges, SparseBound should be used here instead.
59
    // See: https://github.com/conjure-cp/conjure-oxide/issues/84
60

            
61
3
    if ranges.len() != 1 {
62
        return Err(SolverError::NotSupported(
63
            SOLVER,
64
            format!(
65
            "variable {:?} has {:?} ranges. Multiple ranges / SparseBound is not yet supported.",
66
            str_name,
67
            ranges.len()
68
        ),
69
        ));
70
3
    }
71

            
72
3
    let range = ranges.first().ok_or(SolverError::InvalidInstance(
73
3
        SOLVER,
74
3
        format!("variable {:?} has no range", str_name),
75
3
    ))?;
76

            
77
3
    let (low, high) = match range {
78
3
        ConjureRange::Bounded(x, y) => Ok((x.to_owned(), y.to_owned())),
79
        ConjureRange::Single(x) => Ok((x.to_owned(), x.to_owned())),
80
        #[allow(unreachable_patterns)]
81
        x => Err(SolverError::NotSupported(SOLVER, format!("{:?}", x))),
82
    }?;
83

            
84
3
    minion_model
85
3
        .named_variables
86
3
        .add_var(str_name.to_owned(), MinionDomain::Bound(low, high))
87
3
        .ok_or(SolverError::InvalidInstance(
88
3
            SOLVER,
89
3
            format!("variable {:?} is defined twice", str_name),
90
3
        ))?;
91

            
92
3
    Ok(())
93
3
}
94

            
95
1
fn parse_exprs(
96
1
    conjure_model: &ConjureModel,
97
1
    minion_model: &mut MinionModel,
98
1
) -> Result<(), SolverError> {
99
3
    for expr in conjure_model.get_constraints_vec().iter() {
100
3
        parse_expr(expr.to_owned(), minion_model)?;
101
    }
102
1
    Ok(())
103
1
}
104

            
105
3
fn parse_expr(expr: ConjureExpression, minion_model: &mut MinionModel) -> Result<(), SolverError> {
106
3
    match expr {
107
1
        ConjureExpression::SumLeq(lhs, rhs) => parse_sumleq(lhs, *rhs, minion_model),
108
1
        ConjureExpression::SumGeq(lhs, rhs) => parse_sumgeq(lhs, *rhs, minion_model),
109
1
        ConjureExpression::Ineq(a, b, c) => parse_ineq(*a, *b, *c, minion_model),
110
        x => Err(SolverError::NotSupported(SOLVER, format!("{:?}", x))),
111
    }
112
3
}
113

            
114
// fn parse_and(
115
//     expressions: Vec<Expression>,
116
//     minion_model: &mut MinionModel,
117
// ) -> Result<(), SolverError> {
118
//     // ToDo - Nik said that he will do this
119
// }
120

            
121
1
fn parse_sumleq(
122
1
    sum_vars: Vec<ConjureExpression>,
123
1
    rhs: ConjureExpression,
124
1
    minion_model: &mut MinionModel,
125
1
) -> Result<(), SolverError> {
126
1
    let minion_vars = must_be_vars(sum_vars)?;
127
1
    let minion_rhs = must_be_var(rhs)?;
128
1
    minion_model
129
1
        .constraints
130
1
        .push(MinionConstraint::SumLeq(minion_vars, minion_rhs));
131
1

            
132
1
    Ok(())
133
1
}
134

            
135
1
fn parse_sumgeq(
136
1
    sum_vars: Vec<ConjureExpression>,
137
1
    rhs: ConjureExpression,
138
1
    minion_model: &mut MinionModel,
139
1
) -> Result<(), SolverError> {
140
1
    let minion_vars = must_be_vars(sum_vars)?;
141
1
    let minion_rhs = must_be_var(rhs)?;
142
1
    minion_model
143
1
        .constraints
144
1
        .push(MinionConstraint::SumGeq(minion_vars, minion_rhs));
145
1

            
146
1
    Ok(())
147
1
}
148

            
149
1
fn parse_ineq(
150
1
    a: ConjureExpression,
151
1
    b: ConjureExpression,
152
1
    c: ConjureExpression,
153
1
    minion_model: &mut MinionModel,
154
1
) -> Result<(), SolverError> {
155
1
    let a_minion = must_be_var(a)?;
156
1
    let b_minion = must_be_var(b)?;
157
1
    let c_value = must_be_const(c)?;
158
1
    minion_model.constraints.push(MinionConstraint::Ineq(
159
1
        a_minion,
160
1
        b_minion,
161
1
        MinionConstant::Integer(c_value),
162
1
    ));
163
1

            
164
1
    Ok(())
165
1
}
166

            
167
2
fn must_be_vars(exprs: Vec<ConjureExpression>) -> Result<Vec<MinionVar>, SolverError> {
168
2
    let mut minion_vars: Vec<MinionVar> = vec![];
169
8
    for expr in exprs {
170
6
        let minion_var = must_be_var(expr)?;
171
6
        minion_vars.push(minion_var);
172
    }
173
2
    Ok(minion_vars)
174
2
}
175

            
176
10
fn must_be_var(e: ConjureExpression) -> Result<MinionVar, SolverError> {
177
10
    // a minion var is either a reference or a "var as const"
178
10
    match must_be_ref(e.clone()) {
179
8
        Ok(name) => Ok(MinionVar::NameRef(name)),
180
2
        Err(_) => match must_be_const(e) {
181
2
            Ok(n) => Ok(MinionVar::ConstantAsVar(n)),
182
            Err(x) => Err(x),
183
        },
184
    }
185
10
}
186

            
187
10
fn must_be_ref(e: ConjureExpression) -> Result<String, SolverError> {
188
10
    let name = match e {
189
8
        ConjureExpression::Reference(n) => Ok(n),
190
2
        x => Err(SolverError::InvalidInstance(
191
2
            SOLVER,
192
2
            format!("expected a reference, but got `{0:?}`", x),
193
2
        )),
194
2
    }?;
195

            
196
8
    let str_name = name_to_string(name);
197
8
    Ok(str_name)
198
10
}
199

            
200
3
fn must_be_const(e: ConjureExpression) -> Result<i32, SolverError> {
201
3
    match e {
202
3
        ConjureExpression::Constant(ConjureConstant::Int(n)) => Ok(n),
203
        x => Err(SolverError::InvalidInstance(
204
            SOLVER,
205
            format!("expected a constant, but got `{0:?}`", x),
206
        )),
207
    }
208
3
}
209

            
210
11
fn name_to_string(name: ConjureName) -> String {
211
11
    match name {
212
11
        ConjureName::UserName(x) => x,
213
        ConjureName::MachineName(x) => x.to_string(),
214
    }
215
11
}
216

            
217
#[cfg(test)]
218
mod tests {
219
    use anyhow::anyhow;
220
    use conjure_core::ast::Expression;
221
    use std::collections::HashMap;
222

            
223
    use minion_rs::ast::VarName;
224

            
225
    use super::*;
226

            
227
    #[test]
228
1
    fn flat_xyz_model() -> Result<(), anyhow::Error> {
229
1
        // TODO: convert to use public interfaces when these exist.
230
1
        let mut model = ConjureModel {
231
1
            variables: HashMap::new(),
232
1
            constraints: Expression::And(Vec::new()),
233
1
        };
234
1

            
235
1
        add_int_with_range(&mut model, "x", 1, 3)?;
236
1
        add_int_with_range(&mut model, "y", 2, 4)?;
237
1
        add_int_with_range(&mut model, "z", 1, 5)?;
238

            
239
1
        let x = ConjureExpression::Reference(ConjureName::UserName("x".to_owned()));
240
1
        let y = ConjureExpression::Reference(ConjureName::UserName("y".to_owned()));
241
1
        let z = ConjureExpression::Reference(ConjureName::UserName("z".to_owned()));
242
1
        let four = ConjureExpression::Constant(ConjureConstant::Int(4));
243
1

            
244
1
        let geq = ConjureExpression::SumGeq(
245
1
            vec![x.to_owned(), y.to_owned(), z.to_owned()],
246
1
            Box::from(four.to_owned()),
247
1
        );
248
1
        let leq = ConjureExpression::SumLeq(
249
1
            vec![x.to_owned(), y.to_owned(), z.to_owned()],
250
1
            Box::from(four.to_owned()),
251
1
        );
252
1
        let ineq = ConjureExpression::Ineq(Box::from(x), Box::from(y), Box::from(four));
253
1

            
254
1
        model.add_constraints(vec![geq, leq, ineq]);
255

            
256
1
        let minion_model = MinionModel::from_conjure(model)?;
257
1
        Ok(minion_rs::run_minion(minion_model, xyz_callback)?)
258
1
    }
259

            
260
    #[allow(clippy::unwrap_used)]
261
1
    fn xyz_callback(solutions: HashMap<VarName, MinionConstant>) -> bool {
262
1
        let x = match solutions.get("x").unwrap() {
263
1
            MinionConstant::Integer(n) => n,
264
            _ => panic!("x should be a integer"),
265
        };
266

            
267
1
        let y = match solutions.get("y").unwrap() {
268
1
            MinionConstant::Integer(n) => n,
269
            _ => panic!("y should be a integer"),
270
        };
271

            
272
1
        let z = match solutions.get("z").unwrap() {
273
1
            MinionConstant::Integer(n) => n,
274
            _ => panic!("z should be a integer"),
275
        };
276

            
277
1
        assert_eq!(*x, 1);
278
1
        assert_eq!(*y, 2);
279
1
        assert_eq!(*z, 1);
280

            
281
1
        false
282
1
    }
283

            
284
3
    fn add_int_with_range(
285
3
        model: &mut ConjureModel,
286
3
        name: &str,
287
3
        domain_low: i32,
288
3
        domain_high: i32,
289
3
    ) -> Result<(), SolverError> {
290
3
        // TODO: convert to use public interfaces when these exist.
291
3
        let res = model.variables.insert(
292
3
            ConjureName::UserName(name.to_owned()),
293
3
            DecisionVariable {
294
3
                domain: ConjureDomain::IntDomain(vec![ConjureRange::Bounded(
295
3
                    domain_low,
296
3
                    domain_high,
297
3
                )]),
298
3
            },
299
3
        );
300
3
        if res.is_some() {
301
            return Err(SolverError::Other(anyhow!(
302
                "Tried to add variable {:?} to the symbol table, but it was already present",
303
                name
304
            )));
305
3
        }
306
3

            
307
3
        Ok(())
308
3
    }
309
}