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
26fn 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
46fn 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
67fn 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
84fn 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
103fn 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 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 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 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 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 #[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 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 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 *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}