1use std::collections::{BTreeMap, HashMap};
2use std::fmt::Write as _;
3use std::fs;
4use std::path::PathBuf;
5use std::string::ToString;
6use std::sync::{Arc, Mutex, RwLock};
7use std::time::Instant;
8
9use conjure_cp::ast::categories::{Category, CategoryOf};
10use conjure_cp::ast::{Atom, DeclarationPtr, Expression, GroundDomain, Literal, Metadata, Name};
11use conjure_cp::bug_assert;
12use conjure_cp::context::Context;
13use conjure_cp::settings::{configured_rule_trace_enabled, set_rule_trace_enabled};
14
15use serde_json::{Map, Value as JsonValue};
16
17use itertools::Itertools as _;
18use tempfile::tempdir;
19
20use crate::utils::json::sort_json_object;
21use crate::utils::simplified_json::{
22 domains_from_model, param_model_from_assignments, params_from_simplified_json_str,
23 solutions_from_simplified_json_str,
24};
25use conjure_cp::Model;
26use conjure_cp::instantiate::instantiate_model;
27use conjure_cp::parse::tree_sitter::parse_essence_file_native;
28use conjure_cp::representation::util::try_up;
29use conjure_cp::solver::{SearchStatus, Solver};
30
31use glob::glob;
32
33use uniplate::Uniplate;
34
35#[derive(Clone, Copy, Debug, Default)]
36pub struct ConjureRunTimings {
37 pub wall_clock_time_s: f64,
38 pub translation_time_s: f64,
39 pub conjure_translation_time_s: f64,
40 pub savilerow_translation_time_s: f64,
41 pub solve_time_s: f64,
42}
43
44#[derive(Debug)]
45pub struct ConjureSolutions {
46 pub solutions: Vec<BTreeMap<Name, Literal>>,
47 pub timings: Option<ConjureRunTimings>,
48}
49
50fn literal_for_reference_domain(
55 reference_domain: Option<GroundDomain>,
56 value: &Literal,
57) -> Option<Literal> {
58 if matches!(reference_domain, Some(GroundDomain::Bool)) {
59 return match value {
60 Literal::Bool(x) => Some(Literal::Bool(*x)),
61 Literal::Int(1) => Some(Literal::Bool(true)),
62 Literal::Int(0) => Some(Literal::Bool(false)),
63 _ => None,
64 };
65 }
66
67 Some(value.clone())
68}
69
70fn substitute_from_solution(
72 expr: &Expression,
73 previous_solution: &BTreeMap<Name, Literal>,
74) -> Option<Expression> {
75 match expr {
76 Expression::FromSolution(_, atom_expr) => {
77 let Atom::Reference(reference) = atom_expr.as_ref() else {
78 return Some(expr.clone());
79 };
80
81 let name = reference.name();
82 let value = previous_solution.get(&name)?;
83 let reference_domain = reference.resolved_domain().map(|x| x.as_ref().clone());
84 let value = literal_for_reference_domain(reference_domain, value)?;
85 Some(Expression::Atomic(Metadata::new(), Atom::Literal(value)))
86 }
87 _ => Some(expr.clone()),
88 }
89}
90
91fn substitute_current_solution_refs(
93 expr: &Expression,
94 candidate_solution: &BTreeMap<Name, Literal>,
95) -> Option<Expression> {
96 match expr {
97 Expression::Atomic(_, Atom::Reference(reference)) => {
98 let name = reference.name();
99 let value = candidate_solution.get(&name)?;
100 let reference_domain = reference.resolved_domain().map(|x| x.as_ref().clone());
101 let value = literal_for_reference_domain(reference_domain, value)?;
102 Some(Expression::Atomic(Metadata::new(), Atom::Literal(value)))
103 }
104 _ => Some(expr.clone()),
105 }
106}
107
108fn does_solution_dominate(
113 dominance_expression: &Expression,
114 candidate_solution: &BTreeMap<Name, Literal>,
115 previous_solution: &BTreeMap<Name, Literal>,
116) -> bool {
117 let expr = dominance_expression
118 .rewrite(&|e| substitute_from_solution(&e, previous_solution))
119 .rewrite(&|e| substitute_current_solution_refs(&e, candidate_solution));
120
121 matches!(
122 conjure_cp::ast::eval::eval_constant(&expr),
123 Some(Literal::Bool(true))
124 )
125}
126
127fn retroactively_prune_dominated(
129 solutions: Vec<BTreeMap<Name, Literal>>,
130 dominance_expression: &Expression,
131) -> Vec<BTreeMap<Name, Literal>> {
132 solutions
133 .iter()
134 .enumerate()
135 .filter_map(|(i, solution)| {
136 let dominated = solutions.iter().enumerate().any(|(j, candidate)| {
137 i != j && does_solution_dominate(dominance_expression, candidate, solution)
138 });
139
140 if dominated {
141 None
142 } else {
143 Some(solution.clone())
144 }
145 })
146 .collect()
147}
148
149fn validate_solution_collection_options(model: &Model, num_sols: i32) -> Result<(), anyhow::Error> {
150 if model.objective.is_some() && num_sols != 1 {
151 let got = if num_sols == 0 {
152 "all".to_string()
153 } else {
154 num_sols.to_string()
155 };
156 return Err(anyhow::anyhow!(
157 "optimisation problems require number-of-solutions=1 (got {got})"
158 ));
159 }
160 Ok(())
161}
162
163pub fn get_solutions(
164 solver: Solver,
165 model: Model,
166 num_sols: i32,
167 keep_intermediate_solutions: bool,
168 solver_input_file: &Option<PathBuf>,
169 rule_trace_cdp: bool,
170) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
171 set_rule_trace_enabled(rule_trace_cdp && configured_rule_trace_enabled());
172
173 validate_solution_collection_options(&model, num_sols)?;
174
175 let is_optimisation = model.objective.is_some();
176
177 let dominance_expression = model.dominance.as_ref().map(|expr| match expr {
178 Expression::DominanceRelation(_, inner) => inner.as_ref().clone(),
179 _ => expr.clone(),
180 });
181
182 let adaptor_name = solver.get_name();
183
184 tracing::info!(target: "conjure::stage", adaptor = adaptor_name, "building solver model");
185
186 let symbols_ptr = model.symbols_ptr_unchecked().clone();
188
189 let solver = solver.load_model(model)?;
190
191 if let Some(solver_input_file) = solver_input_file {
192 tracing::info!(target: "conjure::stage",
193 "Writing solver input file to {}",
194 solver_input_file.display()
195 );
196 let file = Box::new(std::fs::File::create(solver_input_file)?);
197 solver.write_solver_input_file(&mut (file as Box<dyn std::io::Write>))?;
198 }
199
200 tracing::info!(target: "conjure::stage", adaptor = adaptor_name, "running solver");
201
202 let all_solutions_ref = Arc::new(Mutex::<Vec<BTreeMap<Name, Literal>>>::new(vec![]));
204 let all_solutions_ref_2 = all_solutions_ref.clone();
205
206 let solver = if is_optimisation {
207 solver
208 .solve(Box::new(move |sols| {
209 let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
210 let solution = sols.into_iter().collect();
211 if keep_intermediate_solutions {
212 all_solutions.push(solution);
213 } else {
214 all_solutions.clear();
215 all_solutions.push(solution);
216 }
217 true
218 }))
219 .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
220 } else if num_sols > 0 {
221 let sols_left = Mutex::new(num_sols);
223
224 solver
225 .solve(Box::new(move |sols| {
226 let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
227 all_solutions.push(sols.into_iter().collect());
228 let mut sols_left = sols_left.lock().unwrap();
229 *sols_left -= 1;
230
231 *sols_left != 0
232 }))
233 .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
234 } else {
235 solver
237 .solve(Box::new(move |sols| {
238 let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
239 all_solutions.push(sols.into_iter().collect());
240 true
241 }))
242 .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
243 };
244
245 let search_status = solver.search_status();
246 solver.save_stats_to_context();
247
248 #[allow(clippy::unwrap_used)]
250 let mut sols_guard = (*all_solutions_ref).lock().unwrap();
251 let sols = &mut *sols_guard;
252
253 let requested_limit_reached = !is_optimisation
257 && num_sols > 0
258 && sols.len() >= usize::try_from(num_sols).unwrap_or(usize::MAX);
259 if let SearchStatus::Incomplete(reason) = search_status
260 && !requested_limit_reached
261 {
262 return Err(anyhow::anyhow!("solver search incomplete: {reason:?}"));
263 }
264
265 let symbols = symbols_ptr.read();
266
267 let names = symbols.clone().into_iter().map(|x| x.0).collect_vec();
270 let representations = names
271 .into_iter()
272 .filter_map(|x| symbols.representations_for(&x).map(|repr| (x, repr)))
273 .filter_map(|(name, reprs)| {
274 if reprs.is_empty() {
275 return None;
276 }
277 bug_assert!(
278 reprs.len() <= 1,
279 "multiple representations for a variable is not yet implemented"
280 );
281
282 assert_eq!(
283 reprs[0].len(),
284 1,
285 "nested representations are not yet implemented"
286 );
287 Some((name, reprs[0][0].clone()))
288 })
289 .collect_vec();
290
291 let structured_declarations: Vec<DeclarationPtr> = symbols
292 .iter_local()
293 .filter(|(name, declaration)| {
294 matches!(name, Name::User(_))
295 && declaration.category_of() >= Category::Decision
296 && !declaration.reprs().is_empty()
297 })
298 .map(|(_, declaration)| declaration.clone())
299 .collect();
300
301 for sol in sols.iter_mut() {
302 for (name, representation) in representations.iter() {
304 if sol.contains_key(name) {
305 continue;
306 }
307
308 let value = representation.value_up(sol).map_err(|err| {
309 anyhow::anyhow!(
310 "failed to reconstruct value for variable {name} from solver solution: {err}"
311 )
312 })?;
313 sol.insert(name.clone(), value);
314 }
315
316 let raw_assignment: HashMap<Name, Literal> = sol.clone().into_iter().collect();
317 for declaration in &structured_declarations {
318 let value = try_up(declaration.clone(), &raw_assignment).map_err(|err| {
319 anyhow::anyhow!(
320 "failed to reconstruct value for variable {}: {err}",
321 declaration.name()
322 )
323 })?;
324 sol.insert(declaration.name().clone(), value);
325 }
326
327 *sol = sol
330 .clone()
331 .into_iter()
332 .filter(|(name, _)| {
333 !matches!(name, Name::Represented(_)) && !matches!(name, Name::Machine(_))
334 })
335 .collect();
336 }
337
338 sols.retain(|x| !x.is_empty());
339 if let Some(dominance_expression) = dominance_expression.as_ref() {
340 let pre_prune_len = sols.len();
341 let pruned = retroactively_prune_dominated(sols.clone(), dominance_expression);
342 let post_prune_len = pruned.len();
343
344 tracing::info!(
345 target: "conjure::stage",
346 retained = post_prune_len,
347 considered = pre_prune_len,
348 "pruned dominated solutions"
349 );
350
351 *sols = pruned;
352 }
353
354 Ok(sols.clone())
355}
356
357#[derive(Clone, Debug, Default)]
358pub struct ConjureSolveCaptureOptions {
359 pub artifact_dir: Option<PathBuf>,
361 pub savilerow_options: Option<String>,
363}
364
365#[allow(clippy::unwrap_used)]
366pub fn get_solutions_from_conjure(
367 essence_file: &str,
368 param_file: Option<&str>,
369 context: Arc<RwLock<Context<'static>>>,
370) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
371 Ok(get_solutions_from_conjure_with_stats(
372 essence_file,
373 param_file,
374 context,
375 0,
376 ConjureSolveCaptureOptions::default(),
377 )?
378 .solutions)
379}
380
381#[allow(clippy::unwrap_used)]
382pub fn get_solutions_from_conjure_with_stats(
383 essence_file: &str,
384 param_file: Option<&str>,
385 context: Arc<RwLock<Context<'static>>>,
386 number_of_solutions: i32,
387 capture_options: ConjureSolveCaptureOptions,
388) -> Result<ConjureSolutions, anyhow::Error> {
389 enum ConjureOutputDir {
390 Temp(tempfile::TempDir),
391 Fixed(PathBuf),
392 }
393
394 impl ConjureOutputDir {
395 fn path(&self) -> &std::path::Path {
396 match self {
397 Self::Temp(dir) => dir.path(),
398 Self::Fixed(path) => path,
399 }
400 }
401 }
402
403 let output_dir = match &capture_options.artifact_dir {
404 Some(path) => {
405 if path.exists() {
409 fs::remove_dir_all(path)?;
410 }
411 fs::create_dir_all(path)?;
412 ConjureOutputDir::Fixed(path.clone())
413 }
414 None => ConjureOutputDir::Temp(tempdir()?),
415 };
416
417 let mut cmd = std::process::Command::new("conjure");
418 let number_of_solutions_arg = if number_of_solutions == 0 {
419 "all".to_string()
420 } else {
421 number_of_solutions.to_string()
422 };
423
424 cmd.arg("solve")
425 .arg(format!("--number-of-solutions={number_of_solutions_arg}"))
426 .arg("--copy-solutions=no")
427 .arg("--solutions-in-one-file")
428 .arg("--output-format=json")
429 .arg("-o")
430 .arg(output_dir.path());
431
432 if let Some(options) = &capture_options.savilerow_options {
433 cmd.arg(format!("--savilerow-options={options}"));
434 }
435
436 cmd.arg(essence_file);
437
438 if let Some(file) = param_file {
439 cmd.arg(file);
440 }
441
442 let conjure_solve_start = Instant::now();
443 let output = cmd.output()?;
444 let conjure_solve_wall_time_s = conjure_solve_start.elapsed().as_secs_f64();
445
446 if !output.status.success() {
447 let stderr =
448 String::from_utf8(output.stderr).unwrap_or_else(|e| e.utf8_error().to_string());
449 return Err(anyhow::Error::msg(format!(
450 "Error: `conjure solve` exited with code {}; stderr: {}",
451 output.status, stderr
452 )));
453 }
454
455 let domains = domains_for_conjure_solutions(essence_file, param_file, Arc::clone(&context))?;
456
457 let solutions_files: Vec<_> =
458 glob(&format!("{}/*.solutions.json", output_dir.path().display()))?.collect();
459 if solutions_files.is_empty() {
460 let timings = read_conjure_timings(output_dir.path(), conjure_solve_wall_time_s)?;
462 return Ok(ConjureSolutions {
463 solutions: Vec::new(),
464 timings,
465 });
466 }
467
468 let mut solutions_set = Vec::new();
469 for solutions_file in solutions_files {
470 let solutions_file = solutions_file?;
471 let text = fs::read_to_string(&solutions_file)?;
472 solutions_set.extend(solutions_from_simplified_json_str(&text, &domains)?);
473 }
474
475 let timings = read_conjure_timings(output_dir.path(), conjure_solve_wall_time_s)?;
476
477 Ok(ConjureSolutions {
478 solutions: solutions_set
479 .into_iter()
480 .filter(|x| !x.is_empty())
481 .collect(),
482 timings,
483 })
484}
485
486fn domains_for_conjure_solutions(
487 essence_file: &str,
488 param_file: Option<&str>,
489 context: Arc<RwLock<Context<'static>>>,
490) -> Result<BTreeMap<Name, conjure_cp::ast::DomainPtr>, anyhow::Error> {
491 let problem = parse_essence_file_native(essence_file, Arc::clone(&context))?;
492 let unified = match param_file {
493 Some(param_path) if param_path.ends_with(".json") => {
494 let given_domains = domains_from_model(&problem);
495 let params =
496 params_from_simplified_json_str(&fs::read_to_string(param_path)?, &given_domains)?;
497 let param_model =
498 param_model_from_assignments(params, &given_domains, Arc::clone(&context));
499 instantiate_model(problem, param_model)?
500 }
501 Some(param_path) => {
502 let param_model = parse_essence_file_native(param_path, Arc::clone(&context))?;
503 instantiate_model(problem, param_model)?
504 }
505 None => problem,
506 };
507 Ok(domains_from_model(&unified))
508}
509
510fn read_conjure_timings(
511 path: &std::path::Path,
512 conjure_solve_wall_time_s: f64,
513) -> Result<Option<ConjureRunTimings>, anyhow::Error> {
514 let stats_files: Vec<_> = glob(&format!("{}/*.stats.json", path.display()))?.collect();
515 if stats_files.is_empty() {
516 return Ok(None);
517 }
518
519 let mut timings = ConjureRunTimings {
520 wall_clock_time_s: conjure_solve_wall_time_s,
521 ..Default::default()
522 };
523 for stats_file in stats_files {
524 let stats_file = stats_file?;
525 let stats: JsonValue = serde_json::from_str(&fs::read_to_string(&stats_file)?)?;
526 let savilerow_total_time = stats
527 .pointer("/savilerowInfo/SavileRowTotalTime")
528 .and_then(JsonValue::as_str)
529 .and_then(|value| value.parse::<f64>().ok())
530 .unwrap_or_default();
531 let solve_time = stats
532 .pointer("/savilerowInfo/SolverTotalTime")
533 .and_then(JsonValue::as_str)
534 .and_then(|value| value.parse::<f64>().ok())
535 .or_else(|| {
536 stats
537 .pointer("/savilerowInfo/SolverSolveTime")
538 .and_then(JsonValue::as_str)
539 .and_then(|value| value.parse::<f64>().ok())
540 })
541 .unwrap_or_default();
542
543 timings.savilerow_translation_time_s += savilerow_total_time;
544 timings.solve_time_s += solve_time;
545 }
546
547 timings.conjure_translation_time_s =
548 (conjure_solve_wall_time_s - timings.savilerow_translation_time_s - timings.solve_time_s)
549 .max(0.0);
550 timings.translation_time_s =
551 timings.conjure_translation_time_s + timings.savilerow_translation_time_s;
552
553 Ok(Some(timings))
554}
555
556pub fn solutions_to_json(solutions: &[BTreeMap<Name, Literal>]) -> JsonValue {
557 let json_solutions = solutions.iter().map(solution_to_json).collect();
558 let ans = JsonValue::Array(json_solutions);
559 sort_json_object(&ans, true)
560}
561
562pub fn solutions_to_essence(solutions: &[BTreeMap<Name, Literal>]) -> String {
564 let mut solutions = solutions.iter().collect::<Vec<_>>();
565 solutions.sort_by(|lhs, rhs| solution_essence_cmp(lhs, rhs));
566
567 let mut output = String::new();
568 for (index, solution) in solutions.iter().enumerate() {
569 writeln!(output, "$ Solution: {:06}", index + 1).unwrap();
570 writeln!(output, "language Essence 1.3\n").unwrap();
571 for (name, value) in *solution {
572 writeln!(output, "letting {name} be {value}").unwrap();
573 }
574 output.push_str("\n\n");
575 }
576 output
577}
578
579fn solution_essence_cmp(
580 lhs: &BTreeMap<Name, Literal>,
581 rhs: &BTreeMap<Name, Literal>,
582) -> std::cmp::Ordering {
583 lhs.iter()
584 .zip(rhs)
585 .find_map(|((lhs_name, lhs_value), (rhs_name, rhs_value))| {
586 let ordering = lhs_name.cmp(rhs_name);
587 (ordering != std::cmp::Ordering::Equal)
588 .then_some(ordering)
589 .or_else(|| {
590 let ordering = lhs_value.essence_cmp(rhs_value);
591 (ordering != std::cmp::Ordering::Equal).then_some(ordering)
592 })
593 })
594 .unwrap_or_else(|| lhs.len().cmp(&rhs.len()))
595}
596
597fn solution_to_json(solution: &BTreeMap<Name, Literal>) -> JsonValue {
598 let mut json_solution = Map::new();
599 for (var_name, constant) in solution {
600 let serialized_constant = serde_json::to_value(constant).unwrap();
601 json_solution.insert(var_name.to_string(), serialized_constant);
602 }
603 sort_json_object(&JsonValue::Object(json_solution), false)
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use conjure_cp::ast::{DeclarationPtr, Domain, Moo, Reference};
610
611 #[test]
612 fn renders_conjure_multi_solution_essence_format() {
613 let mut first = BTreeMap::new();
614 first.insert(Name::User("b".into()), Literal::Bool(false));
615 first.insert(Name::User("x".into()), Literal::Int(1));
616 let mut second = BTreeMap::new();
617 second.insert(Name::User("b".into()), Literal::Bool(true));
618 second.insert(Name::User("x".into()), Literal::Int(2));
619
620 assert_eq!(
621 solutions_to_essence(&[first, second]),
622 concat!(
623 "$ Solution: 000001\n",
624 "language Essence 1.3\n\n",
625 "letting b be false\n",
626 "letting x be 1\n\n\n",
627 "$ Solution: 000002\n",
628 "language Essence 1.3\n\n",
629 "letting b be true\n",
630 "letting x be 2\n\n\n",
631 )
632 );
633 }
634
635 #[test]
636 fn retroactive_pruning_removes_dominated_prior_solution() {
637 let x = Name::User("x".into());
638 let x_ref = Expression::Atomic(
639 Metadata::new(),
640 Atom::Reference(Reference::new(DeclarationPtr::new_find(
641 x.clone(),
642 Domain::bool(),
643 ))),
644 );
645 let dominance_expression = Expression::Imply(
646 Metadata::new(),
647 Moo::new(x_ref),
648 Moo::new(Expression::FromSolution(
649 Metadata::new(),
650 Moo::new(Atom::Reference(Reference::new(DeclarationPtr::new_find(
651 x.clone(),
652 Domain::bool(),
653 )))),
654 )),
655 );
656
657 let mut sol_true = BTreeMap::new();
658 sol_true.insert(x.clone(), Literal::Int(1));
659 let mut sol_false = BTreeMap::new();
660 sol_false.insert(x, Literal::Int(0));
661
662 let pruned =
663 retroactively_prune_dominated(vec![sol_true, sol_false.clone()], &dominance_expression);
664
665 assert_eq!(pruned, vec![sol_false]);
666 }
667}