Skip to main content

conjure_cp_cli/utils/
conjure.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3use std::string::ToString;
4use std::sync::{Arc, Mutex, RwLock};
5
6use conjure_cp::ast::{Atom, DeclarationKind, Expression, GroundDomain, Literal, Metadata, Name};
7use conjure_cp::bug;
8use conjure_cp::context::Context;
9use conjure_cp::settings::{configured_rule_trace_enabled, set_rule_trace_enabled};
10
11use serde_json::{Map, Value as JsonValue};
12
13use itertools::Itertools as _;
14use tempfile::tempdir;
15
16use crate::utils::json::sort_json_object;
17use conjure_cp::Model;
18use conjure_cp::parse::tree_sitter::parse_essence_file;
19use conjure_cp::solver::Solver;
20
21use glob::glob;
22
23use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
24use uniplate::Uniplate;
25
26/// Coerces a literal into the type expected by a reference domain when possible.
27///
28/// This is currently used to turn `0`/`1` solver outputs back into boolean
29/// literals before substituting them into dominance expressions.
30fn literal_for_reference_domain(
31    reference_domain: Option<GroundDomain>,
32    value: &Literal,
33) -> Option<Literal> {
34    if matches!(reference_domain, Some(GroundDomain::Bool)) {
35        return match value {
36            Literal::Bool(x) => Some(Literal::Bool(*x)),
37            Literal::Int(1) => Some(Literal::Bool(true)),
38            Literal::Int(0) => Some(Literal::Bool(false)),
39            _ => None,
40        };
41    }
42
43    Some(value.clone())
44}
45
46/// Replaces `fromSolution(x)` occurrences with the value of `x` from a previous solution.
47fn substitute_from_solution(
48    expr: &Expression,
49    previous_solution: &BTreeMap<Name, Literal>,
50) -> Option<Expression> {
51    match expr {
52        Expression::FromSolution(_, atom_expr) => {
53            let Atom::Reference(reference) = atom_expr.as_ref() else {
54                return Some(expr.clone());
55            };
56
57            let name = reference.name();
58            let value = previous_solution.get(&name)?;
59            let reference_domain = reference.resolved_domain().map(|x| x.as_ref().clone());
60            let value = literal_for_reference_domain(reference_domain, value)?;
61            Some(Expression::Atomic(Metadata::new(), Atom::Literal(value)))
62        }
63        _ => Some(expr.clone()),
64    }
65}
66
67/// Replaces direct variable references with values from the candidate solution.
68fn substitute_current_solution_refs(
69    expr: &Expression,
70    candidate_solution: &BTreeMap<Name, Literal>,
71) -> Option<Expression> {
72    match expr {
73        Expression::Atomic(_, Atom::Reference(reference)) => {
74            let name = reference.name();
75            let value = candidate_solution.get(&name)?;
76            let reference_domain = reference.resolved_domain().map(|x| x.as_ref().clone());
77            let value = literal_for_reference_domain(reference_domain, value)?;
78            Some(Expression::Atomic(Metadata::new(), Atom::Literal(value)))
79        }
80        _ => Some(expr.clone()),
81    }
82}
83
84/// Evaluates whether `candidate_solution` dominates `previous_solution`.
85///
86/// The dominance expression is instantiated with values from both solutions and
87/// then constant-folded to a boolean result.
88fn does_solution_dominate(
89    dominance_expression: &Expression,
90    candidate_solution: &BTreeMap<Name, Literal>,
91    previous_solution: &BTreeMap<Name, Literal>,
92) -> bool {
93    let expr = dominance_expression
94        .rewrite(&|e| substitute_from_solution(&e, previous_solution))
95        .rewrite(&|e| substitute_current_solution_refs(&e, candidate_solution));
96
97    matches!(
98        conjure_cp::ast::eval::eval_constant(&expr),
99        Some(Literal::Bool(true))
100    )
101}
102
103/// Removes solutions that are dominated by another solution in the result set.
104fn retroactively_prune_dominated(
105    solutions: Vec<BTreeMap<Name, Literal>>,
106    dominance_expression: &Expression,
107) -> Vec<BTreeMap<Name, Literal>> {
108    solutions
109        .iter()
110        .enumerate()
111        .filter_map(|(i, solution)| {
112            let dominated = solutions.iter().enumerate().any(|(j, candidate)| {
113                i != j && does_solution_dominate(dominance_expression, candidate, solution)
114            });
115
116            if dominated {
117                None
118            } else {
119                Some(solution.clone())
120            }
121        })
122        .collect()
123}
124
125pub fn get_solutions(
126    solver: Solver,
127    model: Model,
128    num_sols: i32,
129    solver_input_file: &Option<PathBuf>,
130    rule_trace_cdp: bool,
131) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
132    set_rule_trace_enabled(rule_trace_cdp && configured_rule_trace_enabled());
133
134    let dominance_expression = model.dominance.as_ref().map(|expr| match expr {
135        Expression::DominanceRelation(_, inner) => inner.as_ref().clone(),
136        _ => expr.clone(),
137    });
138
139    let adaptor_name = solver.get_name();
140
141    eprintln!("Building {adaptor_name} model...");
142
143    // Create for later since we consume the model when loading it
144    let symbols_ptr = model.symbols_ptr_unchecked().clone();
145
146    let solver = solver.load_model(model)?;
147
148    if let Some(solver_input_file) = solver_input_file {
149        eprintln!(
150            "Writing solver input file to {}",
151            solver_input_file.display()
152        );
153        let file = Box::new(std::fs::File::create(solver_input_file)?);
154        solver.write_solver_input_file(&mut (file as Box<dyn std::io::Write>))?;
155    }
156
157    eprintln!("Running {adaptor_name}...");
158
159    // Create two arcs, one to pass into the solver callback, one to get solutions out later
160    let all_solutions_ref = Arc::new(Mutex::<Vec<BTreeMap<Name, Literal>>>::new(vec![]));
161    let all_solutions_ref_2 = all_solutions_ref.clone();
162
163    let solver = if num_sols > 0 {
164        // Get num_sols solutions
165        let sols_left = Mutex::new(num_sols);
166
167        solver
168            .solve(Box::new(move |sols| {
169                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
170                (*all_solutions).push(sols.into_iter().collect());
171                let mut sols_left = sols_left.lock().unwrap();
172                *sols_left -= 1;
173
174                *sols_left != 0
175            }))
176            .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
177    } else {
178        // Get all solutions
179        solver
180            .solve(Box::new(move |sols| {
181                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
182                (*all_solutions).push(sols.into_iter().collect());
183                true
184            }))
185            .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
186    };
187
188    solver.save_stats_to_context();
189
190    // Get the collections of solutions and model symbols
191    #[allow(clippy::unwrap_used)]
192    let mut sols_guard = (*all_solutions_ref).lock().unwrap();
193    let sols = &mut *sols_guard;
194    let symbols = symbols_ptr.read();
195
196    // Get the representations for each variable by name, since some variables are
197    // divided into multiple auxiliary variables(see crate::representation::Representation)
198    let names = symbols.clone().into_iter().map(|x| x.0).collect_vec();
199    let representations = names
200        .into_iter()
201        .filter_map(|x| symbols.representations_for(&x).map(|repr| (x, repr)))
202        .filter_map(|(name, reprs)| {
203            if reprs.is_empty() {
204                return None;
205            }
206            assert!(
207                reprs.len() <= 1,
208                "multiple representations for a variable is not yet implemented"
209            );
210
211            assert_eq!(
212                reprs[0].len(),
213                1,
214                "nested representations are not yet implemented"
215            );
216            Some((name, reprs[0][0].clone()))
217        })
218        .collect_vec();
219
220    for sol in sols.iter_mut() {
221        // Get the value of complex variables using their auxiliary variables
222        for (name, representation) in representations.iter() {
223            let value = representation.value_up(sol).map_err(|err| {
224                anyhow::anyhow!(
225                    "failed to reconstruct value for variable {name} from solver solution: {err}"
226                )
227            })?;
228            sol.insert(name.clone(), value);
229        }
230
231        // Remove auxiliary variables since we've found the value of the
232        // variable they represent
233        *sol = sol
234            .clone()
235            .into_iter()
236            .filter(|(name, _)| {
237                !matches!(name, Name::Represented(_)) && !matches!(name, Name::Machine(_))
238            })
239            .collect();
240    }
241
242    sols.retain(|x| !x.is_empty());
243    if let Some(dominance_expression) = dominance_expression.as_ref() {
244        let pre_prune_len = sols.len();
245        let pruned = retroactively_prune_dominated(sols.clone(), dominance_expression);
246        let post_prune_len = pruned.len();
247
248        eprintln!("Dominance pruning retained {post_prune_len} of {pre_prune_len} solutions.");
249
250        *sols = pruned;
251    }
252
253    Ok(sols.clone())
254}
255
256#[allow(clippy::unwrap_used)]
257pub fn get_solutions_from_conjure(
258    essence_file: &str,
259    param_file: Option<&str>,
260    context: Arc<RwLock<Context<'static>>>,
261) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
262    let tmp_dir = tempdir()?;
263
264    let mut cmd = std::process::Command::new("conjure");
265
266    cmd.arg("solve")
267        .arg("--number-of-solutions=all")
268        .arg("--copy-solutions=no")
269        .arg("-o")
270        .arg(tmp_dir.path())
271        .arg(essence_file);
272
273    if let Some(file) = param_file {
274        cmd.arg(file);
275    }
276
277    let output = cmd.output()?;
278
279    if !output.status.success() {
280        let stderr =
281            String::from_utf8(output.stderr).unwrap_or_else(|e| e.utf8_error().to_string());
282        return Err(anyhow::Error::msg(format!(
283            "Error: `conjure solve` exited with code {}; stderr: {}",
284            output.status, stderr
285        )));
286    }
287
288    let solutions_files: Vec<_> =
289        glob(&format!("{}/*.solution", tmp_dir.path().display()))?.collect();
290
291    let solutions_set: Vec<_> = solutions_files
292        .par_iter()
293        .map(|solutions_file| {
294            let solutions_file = solutions_file.as_ref().unwrap();
295            let model = parse_essence_file(solutions_file.to_str().unwrap(), Arc::clone(&context))
296                .expect("conjure solutions files to be parsable");
297
298            let mut solutions = BTreeMap::new();
299            for (name, decl) in model.symbols().clone().into_iter() {
300                match &decl.kind() as &DeclarationKind {
301                    conjure_cp::ast::DeclarationKind::ValueLetting(expression, _) => {
302                        let literal = expression
303                            .clone()
304                            .into_literal()
305                            .expect("lettings in a solution should only contain literals");
306                        solutions.insert(name, literal);
307                    }
308                    _ => {
309                        bug!("only expect value letting declarations in solutions")
310                    }
311                }
312            }
313            solutions
314        })
315        .collect();
316
317    Ok(solutions_set
318        .into_iter()
319        .filter(|x| !x.is_empty())
320        .collect())
321}
322
323pub fn solutions_to_json(solutions: &Vec<BTreeMap<Name, Literal>>) -> JsonValue {
324    let mut json_solutions = Vec::new();
325    for solution in solutions {
326        let mut json_solution = Map::new();
327        for (var_name, constant) in solution {
328            let serialized_constant = serde_json::to_value(constant).unwrap();
329            json_solution.insert(var_name.to_string(), serialized_constant);
330        }
331        json_solutions.push(JsonValue::Object(json_solution));
332    }
333    let ans = JsonValue::Array(json_solutions);
334    sort_json_object(&ans, true)
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use conjure_cp::ast::{DeclarationPtr, Domain, Moo, Reference};
341
342    #[test]
343    fn retroactive_pruning_removes_dominated_prior_solution() {
344        let x = Name::User("x".into());
345        let x_ref = Expression::Atomic(
346            Metadata::new(),
347            Atom::Reference(Reference::new(DeclarationPtr::new_find(
348                x.clone(),
349                Domain::bool(),
350            ))),
351        );
352        let dominance_expression = Expression::Imply(
353            Metadata::new(),
354            Moo::new(x_ref.clone()),
355            Moo::new(Expression::FromSolution(
356                Metadata::new(),
357                Moo::new(Atom::Reference(Reference::new(DeclarationPtr::new_find(
358                    x.clone(),
359                    Domain::bool(),
360                )))),
361            )),
362        );
363
364        let mut sol_true = BTreeMap::new();
365        sol_true.insert(x.clone(), Literal::Int(1));
366        let mut sol_false = BTreeMap::new();
367        sol_false.insert(x.clone(), Literal::Int(0));
368
369        let pruned =
370            retroactively_prune_dominated(vec![sol_true, sol_false.clone()], &dominance_expression);
371
372        assert_eq!(pruned, vec![sol_false]);
373    }
374}