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
163#[derive(Clone, Copy, Debug, Default)]
165pub struct SolutionCollectionTimings {
166 pub backend_time_s: f64,
167 pub search_time_s: f64,
169 pub solution_processing_time_s: f64,
170}
171
172pub fn get_solutions(
174 solver: Solver,
175 model: Model,
176 num_sols: i32,
177 keep_intermediate_solutions: bool,
178 solver_input_file: &Option<PathBuf>,
179 rule_trace_cdp: bool,
180) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
181 get_solutions_with_timings(
182 solver,
183 model,
184 num_sols,
185 keep_intermediate_solutions,
186 solver_input_file,
187 rule_trace_cdp,
188 )
189 .map(|(solutions, _)| solutions)
190}
191
192pub fn get_solutions_with_timings(
194 solver: Solver,
195 model: Model,
196 num_sols: i32,
197 keep_intermediate_solutions: bool,
198 solver_input_file: &Option<PathBuf>,
199 rule_trace_cdp: bool,
200) -> Result<(Vec<BTreeMap<Name, Literal>>, SolutionCollectionTimings), anyhow::Error> {
201 set_rule_trace_enabled(rule_trace_cdp && configured_rule_trace_enabled());
202
203 validate_solution_collection_options(&model, num_sols)?;
204
205 let is_optimisation = model.objective.is_some();
206
207 let dominance_expression = model.dominance.as_ref().map(|expr| match expr {
208 Expression::DominanceRelation(_, inner) => inner.as_ref().clone(),
209 _ => expr.clone(),
210 });
211
212 let adaptor_name = solver.get_name();
213
214 tracing::info!(target: "conjure::stage", adaptor = adaptor_name, "building solver model");
215
216 let symbols_ptr = model.symbols_ptr_unchecked().clone();
218
219 let backend_started_at = Instant::now();
220 let solver = solver.load_model(model)?;
221 let backend_time_s = backend_started_at.elapsed().as_secs_f64();
222
223 if let Some(solver_input_file) = solver_input_file {
224 tracing::info!(target: "conjure::stage",
225 "Writing solver input file to {}",
226 solver_input_file.display()
227 );
228 let file = Box::new(std::fs::File::create(solver_input_file)?);
229 solver.write_solver_input_file(&mut (file as Box<dyn std::io::Write>))?;
230 }
231
232 tracing::info!(target: "conjure::stage", adaptor = adaptor_name, "running solver");
233
234 let all_solutions_ref = Arc::new(Mutex::<Vec<BTreeMap<Name, Literal>>>::new(vec![]));
236 let all_solutions_ref_2 = all_solutions_ref.clone();
237
238 let search_started_at = Instant::now();
239 let solver = if is_optimisation {
240 solver
241 .solve(Box::new(move |sols| {
242 let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
243 let solution = sols.into_iter().collect();
244 if keep_intermediate_solutions {
245 all_solutions.push(solution);
246 } else {
247 all_solutions.clear();
248 all_solutions.push(solution);
249 }
250 true
251 }))
252 .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
253 } else if num_sols > 0 {
254 let sols_left = Mutex::new(num_sols);
256
257 solver
258 .solve(Box::new(move |sols| {
259 let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
260 all_solutions.push(sols.into_iter().collect());
261 let mut sols_left = sols_left.lock().unwrap();
262 *sols_left -= 1;
263
264 *sols_left != 0
265 }))
266 .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
267 } else {
268 solver
270 .solve(Box::new(move |sols| {
271 let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
272 all_solutions.push(sols.into_iter().collect());
273 true
274 }))
275 .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
276 };
277
278 let search_time_s = search_started_at.elapsed().as_secs_f64();
279 let processing_started_at = Instant::now();
280 let search_status = solver.search_status();
281 solver.save_stats_to_context();
282
283 #[allow(clippy::unwrap_used)]
285 let mut sols_guard = (*all_solutions_ref).lock().unwrap();
286 let sols = &mut *sols_guard;
287
288 let requested_limit_reached = !is_optimisation
292 && num_sols > 0
293 && sols.len() >= usize::try_from(num_sols).unwrap_or(usize::MAX);
294 if let SearchStatus::Incomplete(reason) = search_status
295 && !requested_limit_reached
296 {
297 return Err(anyhow::anyhow!("solver search incomplete: {reason:?}"));
298 }
299
300 let symbols = symbols_ptr.read();
301
302 let names = symbols.clone().into_iter().map(|x| x.0).collect_vec();
305 let representations = names
306 .into_iter()
307 .filter_map(|x| symbols.representations_for(&x).map(|repr| (x, repr)))
308 .filter_map(|(name, reprs)| {
309 if reprs.is_empty() {
310 return None;
311 }
312 bug_assert!(
313 reprs.len() <= 1,
314 "multiple representations for a variable is not yet implemented"
315 );
316
317 assert_eq!(
318 reprs[0].len(),
319 1,
320 "nested representations are not yet implemented"
321 );
322 Some((name, reprs[0][0].clone()))
323 })
324 .collect_vec();
325
326 let structured_declarations: Vec<DeclarationPtr> = symbols
327 .iter_local()
328 .filter(|(name, declaration)| {
329 matches!(name, Name::User(_))
333 && !declaration.reprs().is_empty()
334 && declaration.category_of() >= Category::Decision
335 })
336 .map(|(_, declaration)| declaration.clone())
337 .collect();
338
339 for sol in sols.iter_mut() {
340 for (name, representation) in representations.iter() {
342 if sol.contains_key(name) {
343 continue;
344 }
345
346 let value = representation.value_up(sol).map_err(|err| {
347 anyhow::anyhow!(
348 "failed to reconstruct value for variable {name} from solver solution: {err}"
349 )
350 })?;
351 sol.insert(name.clone(), value);
352 }
353
354 let raw_assignment: HashMap<Name, Literal> = sol.clone().into_iter().collect();
355 for declaration in &structured_declarations {
356 let value = try_up(declaration.clone(), &raw_assignment).map_err(|err| {
357 anyhow::anyhow!(
358 "failed to reconstruct value for variable {}: {err}",
359 declaration.name()
360 )
361 })?;
362 sol.insert(declaration.name().clone(), value);
363 }
364
365 *sol = sol
368 .clone()
369 .into_iter()
370 .filter(|(name, _)| {
371 !matches!(name, Name::Represented(_)) && !matches!(name, Name::Machine(_))
372 })
373 .collect();
374 }
375
376 sols.retain(|x| !x.is_empty());
377 if let Some(dominance_expression) = dominance_expression.as_ref() {
378 let pre_prune_len = sols.len();
379 let pruned = retroactively_prune_dominated(sols.clone(), dominance_expression);
380 let post_prune_len = pruned.len();
381
382 tracing::info!(
383 target: "conjure::stage",
384 retained = post_prune_len,
385 considered = pre_prune_len,
386 "pruned dominated solutions"
387 );
388
389 *sols = pruned;
390 }
391
392 let solutions = sols.clone();
393 Ok((
394 solutions,
395 SolutionCollectionTimings {
396 backend_time_s,
397 search_time_s,
398 solution_processing_time_s: processing_started_at.elapsed().as_secs_f64(),
399 },
400 ))
401}
402
403#[derive(Clone, Debug, Default)]
404pub struct ConjureSolveCaptureOptions {
405 pub artifact_dir: Option<PathBuf>,
407 pub savilerow_options: Option<String>,
409}
410
411#[allow(clippy::unwrap_used)]
412pub fn get_solutions_from_conjure(
413 essence_file: &str,
414 param_file: Option<&str>,
415 context: Arc<RwLock<Context<'static>>>,
416) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
417 Ok(get_solutions_from_conjure_with_stats(
418 essence_file,
419 param_file,
420 context,
421 0,
422 ConjureSolveCaptureOptions::default(),
423 )?
424 .solutions)
425}
426
427#[allow(clippy::unwrap_used)]
428pub fn get_solutions_from_conjure_with_stats(
429 essence_file: &str,
430 param_file: Option<&str>,
431 context: Arc<RwLock<Context<'static>>>,
432 number_of_solutions: i32,
433 capture_options: ConjureSolveCaptureOptions,
434) -> Result<ConjureSolutions, anyhow::Error> {
435 enum ConjureOutputDir {
436 Temp(tempfile::TempDir),
437 Fixed(PathBuf),
438 }
439
440 impl ConjureOutputDir {
441 fn path(&self) -> &std::path::Path {
442 match self {
443 Self::Temp(dir) => dir.path(),
444 Self::Fixed(path) => path,
445 }
446 }
447 }
448
449 let output_dir = match &capture_options.artifact_dir {
450 Some(path) => {
451 if path.exists() {
455 fs::remove_dir_all(path)?;
456 }
457 fs::create_dir_all(path)?;
458 ConjureOutputDir::Fixed(path.clone())
459 }
460 None => ConjureOutputDir::Temp(tempdir()?),
461 };
462
463 let mut cmd = std::process::Command::new("conjure");
464 let number_of_solutions_arg = if number_of_solutions == 0 {
465 "all".to_string()
466 } else {
467 number_of_solutions.to_string()
468 };
469
470 cmd.arg("solve")
471 .arg(format!("--number-of-solutions={number_of_solutions_arg}"))
472 .arg("--copy-solutions=no")
473 .arg("--solutions-in-one-file")
474 .arg("--output-format=json")
475 .arg("-o")
476 .arg(output_dir.path());
477
478 if let Some(options) = &capture_options.savilerow_options {
479 cmd.arg(format!("--savilerow-options={options}"));
480 }
481
482 cmd.arg(essence_file);
483
484 if let Some(file) = param_file {
485 cmd.arg(file);
486 }
487
488 let conjure_solve_start = Instant::now();
489 let output = cmd.output()?;
490 let conjure_solve_wall_time_s = conjure_solve_start.elapsed().as_secs_f64();
491
492 if !output.status.success() {
493 let stderr =
494 String::from_utf8(output.stderr).unwrap_or_else(|e| e.utf8_error().to_string());
495 return Err(anyhow::Error::msg(format!(
496 "Error: `conjure solve` exited with code {}; stderr: {}",
497 output.status, stderr
498 )));
499 }
500
501 let domains = domains_for_conjure_solutions(essence_file, param_file, Arc::clone(&context))?;
502
503 let solutions_files: Vec<_> =
504 glob(&format!("{}/*.solutions.json", output_dir.path().display()))?.collect();
505 if solutions_files.is_empty() {
506 let timings = read_conjure_timings(output_dir.path(), conjure_solve_wall_time_s)?;
508 return Ok(ConjureSolutions {
509 solutions: Vec::new(),
510 timings,
511 });
512 }
513
514 let mut solutions_set = Vec::new();
515 for solutions_file in solutions_files {
516 let solutions_file = solutions_file?;
517 let text = fs::read_to_string(&solutions_file)?;
518 solutions_set.extend(solutions_from_simplified_json_str(&text, &domains)?);
519 }
520
521 let timings = read_conjure_timings(output_dir.path(), conjure_solve_wall_time_s)?;
522
523 Ok(ConjureSolutions {
524 solutions: solutions_set
525 .into_iter()
526 .filter(|x| !x.is_empty())
527 .collect(),
528 timings,
529 })
530}
531
532fn domains_for_conjure_solutions(
533 essence_file: &str,
534 param_file: Option<&str>,
535 context: Arc<RwLock<Context<'static>>>,
536) -> Result<BTreeMap<Name, conjure_cp::ast::DomainPtr>, anyhow::Error> {
537 let problem = parse_essence_file_native(essence_file, Arc::clone(&context))?;
538 let unified = match param_file {
539 Some(param_path) if param_path.ends_with(".json") => {
540 let given_domains = domains_from_model(&problem);
541 let params =
542 params_from_simplified_json_str(&fs::read_to_string(param_path)?, &given_domains)?;
543 let param_model =
544 param_model_from_assignments(params, &given_domains, Arc::clone(&context));
545 instantiate_model(problem, param_model)?
546 }
547 Some(param_path) => {
548 let param_model = parse_essence_file_native(param_path, Arc::clone(&context))?;
549 instantiate_model(problem, param_model)?
550 }
551 None => problem,
552 };
553 Ok(domains_from_model(&unified))
554}
555
556fn read_conjure_timings(
557 path: &std::path::Path,
558 conjure_solve_wall_time_s: f64,
559) -> Result<Option<ConjureRunTimings>, anyhow::Error> {
560 let stats_files: Vec<_> = glob(&format!("{}/*.stats.json", path.display()))?.collect();
561 if stats_files.is_empty() {
562 return Ok(None);
563 }
564
565 let mut timings = ConjureRunTimings {
566 wall_clock_time_s: conjure_solve_wall_time_s,
567 ..Default::default()
568 };
569 for stats_file in stats_files {
570 let stats_file = stats_file?;
571 let stats: JsonValue = serde_json::from_str(&fs::read_to_string(&stats_file)?)?;
572 let savilerow_total_time = stats
573 .pointer("/savilerowInfo/SavileRowTotalTime")
574 .and_then(JsonValue::as_str)
575 .and_then(|value| value.parse::<f64>().ok())
576 .unwrap_or_default();
577 let solve_time = stats
578 .pointer("/savilerowInfo/SolverTotalTime")
579 .and_then(JsonValue::as_str)
580 .and_then(|value| value.parse::<f64>().ok())
581 .or_else(|| {
582 stats
583 .pointer("/savilerowInfo/SolverSolveTime")
584 .and_then(JsonValue::as_str)
585 .and_then(|value| value.parse::<f64>().ok())
586 })
587 .unwrap_or_default();
588
589 timings.savilerow_translation_time_s += savilerow_total_time;
590 timings.solve_time_s += solve_time;
591 }
592
593 timings.conjure_translation_time_s =
594 (conjure_solve_wall_time_s - timings.savilerow_translation_time_s - timings.solve_time_s)
595 .max(0.0);
596 timings.translation_time_s =
597 timings.conjure_translation_time_s + timings.savilerow_translation_time_s;
598
599 Ok(Some(timings))
600}
601
602pub fn solutions_to_json(solutions: &[BTreeMap<Name, Literal>]) -> JsonValue {
603 let json_solutions = solutions.iter().map(solution_to_json).collect();
604 let ans = JsonValue::Array(json_solutions);
605 sort_json_object(&ans, true)
606}
607
608pub fn solutions_to_essence(solutions: &[BTreeMap<Name, Literal>]) -> String {
610 let mut solutions = solutions.iter().collect::<Vec<_>>();
611 solutions.sort_by(|lhs, rhs| solution_essence_cmp(lhs, rhs));
612
613 let mut output = String::new();
614 for (index, solution) in solutions.iter().enumerate() {
615 writeln!(output, "$ Solution: {:06}", index + 1).unwrap();
616 writeln!(output, "language Essence 1.3\n").unwrap();
617 let mut unnamed_types = BTreeMap::new();
618 for value in solution.values() {
619 for literal in value.universe() {
620 if let Literal::Unnamed(value) = literal {
621 unnamed_types.insert(value.ty.name.clone(), value.ty.size);
622 }
623 }
624 }
625 for (name, size) in unnamed_types {
626 let members = (1..=size).map(|index| format!("{name}_{index}")).join(", ");
627 writeln!(output, "letting {name} be new type enum {{{members}}}").unwrap();
628 }
629 for (name, value) in *solution {
630 writeln!(output, "letting {name} be {value}").unwrap();
631 }
632 output.push_str("\n\n");
633 }
634 output
635}
636
637fn solution_essence_cmp(
638 lhs: &BTreeMap<Name, Literal>,
639 rhs: &BTreeMap<Name, Literal>,
640) -> std::cmp::Ordering {
641 lhs.iter()
642 .zip(rhs)
643 .find_map(|((lhs_name, lhs_value), (rhs_name, rhs_value))| {
644 let ordering = lhs_name.cmp(rhs_name);
645 (ordering != std::cmp::Ordering::Equal)
646 .then_some(ordering)
647 .or_else(|| {
648 let ordering = lhs_value.essence_cmp(rhs_value);
649 (ordering != std::cmp::Ordering::Equal).then_some(ordering)
650 })
651 })
652 .unwrap_or_else(|| lhs.len().cmp(&rhs.len()))
653}
654
655fn solution_to_json(solution: &BTreeMap<Name, Literal>) -> JsonValue {
656 let mut json_solution = Map::new();
657 for (var_name, constant) in solution {
658 let serialized_constant = serde_json::to_value(constant).unwrap();
659 json_solution.insert(var_name.to_string(), serialized_constant);
660 }
661 sort_json_object(&JsonValue::Object(json_solution), false)
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use conjure_cp::ast::{DeclarationPtr, Domain, Moo, Reference};
668
669 #[test]
670 fn renders_conjure_multi_solution_essence_format() {
671 let mut first = BTreeMap::new();
672 first.insert(Name::User("b".into()), Literal::Bool(false));
673 first.insert(Name::User("x".into()), Literal::Int(1));
674 let mut second = BTreeMap::new();
675 second.insert(Name::User("b".into()), Literal::Bool(true));
676 second.insert(Name::User("x".into()), Literal::Int(2));
677
678 assert_eq!(
679 solutions_to_essence(&[first, second]),
680 concat!(
681 "$ Solution: 000001\n",
682 "language Essence 1.3\n\n",
683 "letting b be false\n",
684 "letting x be 1\n\n\n",
685 "$ Solution: 000002\n",
686 "language Essence 1.3\n\n",
687 "letting b be true\n",
688 "letting x be 2\n\n\n",
689 )
690 );
691 }
692
693 #[test]
694 fn retroactive_pruning_removes_dominated_prior_solution() {
695 let x = Name::User("x".into());
696 let x_ref = Expression::Atomic(
697 Metadata::new(),
698 Atom::Reference(Reference::new(DeclarationPtr::new_find(
699 x.clone(),
700 Domain::bool(),
701 ))),
702 );
703 let dominance_expression = Expression::Imply(
704 Metadata::new(),
705 Moo::new(x_ref),
706 Moo::new(Expression::FromSolution(
707 Metadata::new(),
708 Moo::new(Atom::Reference(Reference::new(DeclarationPtr::new_find(
709 x.clone(),
710 Domain::bool(),
711 )))),
712 )),
713 );
714
715 let mut sol_true = BTreeMap::new();
716 sol_true.insert(x.clone(), Literal::Int(1));
717 let mut sol_false = BTreeMap::new();
718 sol_false.insert(x, Literal::Int(0));
719
720 let pruned =
721 retroactively_prune_dominated(vec![sol_true, sol_false.clone()], &dominance_expression);
722
723 assert_eq!(pruned, vec![sol_false]);
724 }
725}