1
use std::collections::{BTreeMap, HashMap};
2
use std::fmt::Write as _;
3
use std::fs;
4
use std::path::PathBuf;
5
use std::string::ToString;
6
use std::sync::{Arc, Mutex, RwLock};
7
use std::time::Instant;
8

            
9
use conjure_cp::ast::categories::{Category, CategoryOf};
10
use conjure_cp::ast::{Atom, DeclarationPtr, Expression, GroundDomain, Literal, Metadata, Name};
11
use conjure_cp::bug_assert;
12
use conjure_cp::context::Context;
13
use conjure_cp::settings::{configured_rule_trace_enabled, set_rule_trace_enabled};
14

            
15
use serde_json::{Map, Value as JsonValue};
16

            
17
use itertools::Itertools as _;
18
use tempfile::tempdir;
19

            
20
use crate::utils::json::sort_json_object;
21
use crate::utils::simplified_json::{
22
    domains_from_model, param_model_from_assignments, params_from_simplified_json_str,
23
    solutions_from_simplified_json_str,
24
};
25
use conjure_cp::Model;
26
use conjure_cp::instantiate::instantiate_model;
27
use conjure_cp::parse::tree_sitter::parse_essence_file_native;
28
use conjure_cp::representation::util::try_up;
29
use conjure_cp::solver::{SearchStatus, Solver};
30

            
31
use glob::glob;
32

            
33
use uniplate::Uniplate;
34

            
35
#[derive(Clone, Copy, Debug, Default)]
36
pub 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)]
45
pub struct ConjureSolutions {
46
    pub solutions: Vec<BTreeMap<Name, Literal>>,
47
    pub timings: Option<ConjureRunTimings>,
48
}
49

            
50
/// Coerces a literal into the type expected by a reference domain when possible.
51
///
52
/// This is currently used to turn `0`/`1` solver outputs back into boolean
53
/// literals before substituting them into dominance expressions.
54
4921060
fn literal_for_reference_domain(
55
4921060
    reference_domain: Option<GroundDomain>,
56
4921060
    value: &Literal,
57
4921060
) -> Option<Literal> {
58
4921060
    if matches!(reference_domain, Some(GroundDomain::Bool)) {
59
4159684
        return match value {
60
640
            Literal::Bool(x) => Some(Literal::Bool(*x)),
61
2168130
            Literal::Int(1) => Some(Literal::Bool(true)),
62
1990914
            Literal::Int(0) => Some(Literal::Bool(false)),
63
            _ => None,
64
        };
65
761376
    }
66

            
67
761376
    Some(value.clone())
68
4921060
}
69

            
70
/// Replaces `fromSolution(x)` occurrences with the value of `x` from a previous solution.
71
15781822
fn substitute_from_solution(
72
15781822
    expr: &Expression,
73
15781822
    previous_solution: &BTreeMap<Name, Literal>,
74
15781822
) -> Option<Expression> {
75
15781822
    match expr {
76
        Expression::FromSolution(_, atom_expr) => {
77
2460530
            let Atom::Reference(reference) = atom_expr.as_ref() else {
78
                return Some(expr.clone());
79
            };
80

            
81
2460530
            let name = reference.name();
82
2460530
            let value = previous_solution.get(&name)?;
83
2460530
            let reference_domain = reference.resolved_domain().map(|x| x.as_ref().clone());
84
2460530
            let value = literal_for_reference_domain(reference_domain, value)?;
85
2460530
            Some(Expression::Atomic(Metadata::new(), Atom::Literal(value)))
86
        }
87
13321292
        _ => Some(expr.clone()),
88
    }
89
15781822
}
90

            
91
/// Replaces direct variable references with values from the candidate solution.
92
15781822
fn substitute_current_solution_refs(
93
15781822
    expr: &Expression,
94
15781822
    candidate_solution: &BTreeMap<Name, Literal>,
95
15781822
) -> Option<Expression> {
96
15781822
    match expr {
97
        Expression::Atomic(_, Atom::Reference(reference)) => {
98
2460530
            let name = reference.name();
99
2460530
            let value = candidate_solution.get(&name)?;
100
2460530
            let reference_domain = reference.resolved_domain().map(|x| x.as_ref().clone());
101
2460530
            let value = literal_for_reference_domain(reference_domain, value)?;
102
2460530
            Some(Expression::Atomic(Metadata::new(), Atom::Literal(value)))
103
        }
104
13321292
        _ => Some(expr.clone()),
105
    }
106
15781822
}
107

            
108
/// Evaluates whether `candidate_solution` dominates `previous_solution`.
109
///
110
/// The dominance expression is instantiated with values from both solutions and
111
/// then constant-folded to a boolean result.
112
237966
fn does_solution_dominate(
113
237966
    dominance_expression: &Expression,
114
237966
    candidate_solution: &BTreeMap<Name, Literal>,
115
237966
    previous_solution: &BTreeMap<Name, Literal>,
116
237966
) -> bool {
117
237966
    let expr = dominance_expression
118
15781822
        .rewrite(&|e| substitute_from_solution(&e, previous_solution))
119
15781822
        .rewrite(&|e| substitute_current_solution_refs(&e, candidate_solution));
120

            
121
237966
    matches!(
122
237966
        conjure_cp::ast::eval::eval_constant(&expr),
123
        Some(Literal::Bool(true))
124
    )
125
237966
}
126

            
127
/// Removes solutions that are dominated by another solution in the result set.
128
2473
fn retroactively_prune_dominated(
129
2473
    solutions: Vec<BTreeMap<Name, Literal>>,
130
2473
    dominance_expression: &Expression,
131
2473
) -> Vec<BTreeMap<Name, Literal>> {
132
2473
    solutions
133
2473
        .iter()
134
2473
        .enumerate()
135
18942
        .filter_map(|(i, solution)| {
136
256908
            let dominated = solutions.iter().enumerate().any(|(j, candidate)| {
137
256908
                i != j && does_solution_dominate(dominance_expression, candidate, solution)
138
256908
            });
139

            
140
18942
            if dominated {
141
6113
                None
142
            } else {
143
12829
                Some(solution.clone())
144
            }
145
18942
        })
146
        .collect()
147
2473
}
148

            
149
167210
fn validate_solution_collection_options(model: &Model, num_sols: i32) -> Result<(), anyhow::Error> {
150
167210
    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
167210
    }
160
167210
    Ok(())
161
167210
}
162

            
163
/// Wall-clock phases of collecting solutions from a compiled model.
164
#[derive(Clone, Copy, Debug, Default)]
165
pub struct SolutionCollectionTimings {
166
    pub backend_time_s: f64,
167
    /// Time inside the solver call, including solution callbacks and solver-time rewrites.
168
    pub search_time_s: f64,
169
    pub solution_processing_time_s: f64,
170
}
171

            
172
/// Collect and reconstruct model solutions.
173
106
pub fn get_solutions(
174
106
    solver: Solver,
175
106
    model: Model,
176
106
    num_sols: i32,
177
106
    keep_intermediate_solutions: bool,
178
106
    solver_input_file: &Option<PathBuf>,
179
106
    rule_trace_cdp: bool,
180
106
) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
181
106
    get_solutions_with_timings(
182
106
        solver,
183
106
        model,
184
106
        num_sols,
185
106
        keep_intermediate_solutions,
186
106
        solver_input_file,
187
106
        rule_trace_cdp,
188
106
    )
189
    .map(|(solutions, _)| solutions)
190
106
}
191

            
192
/// Collect solutions together with measured backend, search and reconstruction times.
193
167210
pub fn get_solutions_with_timings(
194
167210
    solver: Solver,
195
167210
    model: Model,
196
167210
    num_sols: i32,
197
167210
    keep_intermediate_solutions: bool,
198
167210
    solver_input_file: &Option<PathBuf>,
199
167210
    rule_trace_cdp: bool,
200
167210
) -> Result<(Vec<BTreeMap<Name, Literal>>, SolutionCollectionTimings), anyhow::Error> {
201
167210
    set_rule_trace_enabled(rule_trace_cdp && configured_rule_trace_enabled());
202

            
203
167210
    validate_solution_collection_options(&model, num_sols)?;
204

            
205
167210
    let is_optimisation = model.objective.is_some();
206

            
207
167210
    let dominance_expression = model.dominance.as_ref().map(|expr| match expr {
208
2472
        Expression::DominanceRelation(_, inner) => inner.as_ref().clone(),
209
        _ => expr.clone(),
210
2472
    });
211

            
212
167210
    let adaptor_name = solver.get_name();
213

            
214
167210
    tracing::info!(target: "conjure::stage", adaptor = adaptor_name, "building solver model");
215

            
216
    // Create for later since we consume the model when loading it
217
167210
    let symbols_ptr = model.symbols_ptr_unchecked().clone();
218

            
219
167210
    let backend_started_at = Instant::now();
220
167210
    let solver = solver.load_model(model)?;
221
167210
    let backend_time_s = backend_started_at.elapsed().as_secs_f64();
222

            
223
167210
    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
167210
    }
231

            
232
167210
    tracing::info!(target: "conjure::stage", adaptor = adaptor_name, "running solver");
233

            
234
    // Create two arcs, one to pass into the solver callback, one to get solutions out later
235
167210
    let all_solutions_ref = Arc::new(Mutex::<Vec<BTreeMap<Name, Literal>>>::new(vec![]));
236
167210
    let all_solutions_ref_2 = all_solutions_ref.clone();
237

            
238
167210
    let search_started_at = Instant::now();
239
167210
    let solver = if is_optimisation {
240
2948
        solver
241
5222
            .solve(Box::new(move |sols| {
242
5222
                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
243
5222
                let solution = sols.into_iter().collect();
244
5222
                if keep_intermediate_solutions {
245
596
                    all_solutions.push(solution);
246
                } else {
247
4626
                    all_solutions.clear();
248
4626
                    all_solutions.push(solution);
249
                }
250
5222
                true
251
5222
            }))
252
            .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
253
164262
    } else if num_sols > 0 {
254
        // Get num_sols solutions
255
634
        let sols_left = Mutex::new(num_sols);
256

            
257
634
        solver
258
634
            .solve(Box::new(move |sols| {
259
630
                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
260
630
                all_solutions.push(sols.into_iter().collect());
261
630
                let mut sols_left = sols_left.lock().unwrap();
262
630
                *sols_left -= 1;
263

            
264
630
                *sols_left != 0
265
630
            }))
266
            .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
267
    } else {
268
        // Get all solutions
269
163628
        solver
270
1940632
            .solve(Box::new(move |sols| {
271
1940632
                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
272
1940632
                all_solutions.push(sols.into_iter().collect());
273
1940632
                true
274
1940632
            }))
275
            .map_err(|err| anyhow::anyhow!("solver failed while collecting solutions: {err}"))?
276
    };
277

            
278
167210
    let search_time_s = search_started_at.elapsed().as_secs_f64();
279
167210
    let processing_started_at = Instant::now();
280
167210
    let search_status = solver.search_status();
281
167210
    solver.save_stats_to_context();
282

            
283
    // Get the collections of solutions and model symbols
284
    #[allow(clippy::unwrap_used)]
285
167210
    let mut sols_guard = (*all_solutions_ref).lock().unwrap();
286
167210
    let sols = &mut *sols_guard;
287

            
288
    // Stopping after a requested finite number of satisfaction solutions is intentional. Every
289
    // other incomplete search -- notably a solver timeout or interrupt while proving UNSAT -- must
290
    // not be accepted as a complete (possibly empty) solution set.
291
167210
    let requested_limit_reached = !is_optimisation
292
164262
        && num_sols > 0
293
634
        && sols.len() >= usize::try_from(num_sols).unwrap_or(usize::MAX);
294
167210
    if let SearchStatus::Incomplete(reason) = search_status
295
630
        && !requested_limit_reached
296
    {
297
        return Err(anyhow::anyhow!("solver search incomplete: {reason:?}"));
298
167210
    }
299

            
300
167210
    let symbols = symbols_ptr.read();
301

            
302
    // Get the representations for each variable by name, since some variables are
303
    // divided into multiple auxiliary variables(see crate::representation::Representation)
304
86403572
    let names = symbols.clone().into_iter().map(|x| x.0).collect_vec();
305
167210
    let representations = names
306
167210
        .into_iter()
307
86403572
        .filter_map(|x| symbols.representations_for(&x).map(|repr| (x, repr)))
308
86260138
        .filter_map(|(name, reprs)| {
309
86260138
            if reprs.is_empty() {
310
86260138
                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
86260138
        })
324
        .collect_vec();
325

            
326
167210
    let structured_declarations: Vec<DeclarationPtr> = symbols
327
167210
        .iter_local()
328
86403572
        .filter(|(name, declaration)| {
329
            // `category_of` follows a value letting into its body, and from there into every
330
            // letting that body names. Those declarations have no representations, so skip them
331
            // before walking the chain.
332
86403572
            matches!(name, Name::User(_))
333
447476
                && !declaration.reprs().is_empty()
334
299172
                && declaration.category_of() >= Category::Decision
335
86403572
        })
336
291422
        .map(|(_, declaration)| declaration.clone())
337
        .collect();
338

            
339
167210
    for sol in sols.iter_mut() {
340
        // Get the value of complex variables using their auxiliary variables
341
1944214
        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
1944214
        let raw_assignment: HashMap<Name, Literal> = sol.clone().into_iter().collect();
355
1944214
        for declaration in &structured_declarations {
356
4597918
            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
4597918
            sol.insert(declaration.name().clone(), value);
363
        }
364

            
365
        // Remove auxiliary variables since we've found the value of the
366
        // variable they represent
367
1944214
        *sol = sol
368
1944214
            .clone()
369
1944214
            .into_iter()
370
44817964
            .filter(|(name, _)| {
371
44817964
                !matches!(name, Name::Represented(_)) && !matches!(name, Name::Machine(_))
372
44817964
            })
373
            .collect();
374
    }
375

            
376
1944214
    sols.retain(|x| !x.is_empty());
377
167210
    if let Some(dominance_expression) = dominance_expression.as_ref() {
378
2472
        let pre_prune_len = sols.len();
379
2472
        let pruned = retroactively_prune_dominated(sols.clone(), dominance_expression);
380
2472
        let post_prune_len = pruned.len();
381

            
382
2472
        tracing::info!(
383
            target: "conjure::stage",
384
            retained = post_prune_len,
385
            considered = pre_prune_len,
386
            "pruned dominated solutions"
387
        );
388

            
389
2472
        *sols = pruned;
390
164738
    }
391

            
392
167210
    let solutions = sols.clone();
393
167210
    Ok((
394
167210
        solutions,
395
167210
        SolutionCollectionTimings {
396
167210
            backend_time_s,
397
167210
            search_time_s,
398
167210
            solution_processing_time_s: processing_started_at.elapsed().as_secs_f64(),
399
167210
        },
400
167210
    ))
401
167210
}
402

            
403
#[derive(Clone, Debug, Default)]
404
pub struct ConjureSolveCaptureOptions {
405
    /// When set, `conjure solve -o` writes models and Minion files here instead of a temp dir.
406
    pub artifact_dir: Option<PathBuf>,
407
    /// Passed to `conjure solve --savilerow-options` (e.g. `-O0`).
408
    pub savilerow_options: Option<String>,
409
}
410

            
411
#[allow(clippy::unwrap_used)]
412
2
pub fn get_solutions_from_conjure(
413
2
    essence_file: &str,
414
2
    param_file: Option<&str>,
415
2
    context: Arc<RwLock<Context<'static>>>,
416
2
) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
417
2
    Ok(get_solutions_from_conjure_with_stats(
418
2
        essence_file,
419
2
        param_file,
420
2
        context,
421
2
        0,
422
2
        ConjureSolveCaptureOptions::default(),
423
2
    )?
424
    .solutions)
425
2
}
426

            
427
#[allow(clippy::unwrap_used)]
428
2
pub fn get_solutions_from_conjure_with_stats(
429
2
    essence_file: &str,
430
2
    param_file: Option<&str>,
431
2
    context: Arc<RwLock<Context<'static>>>,
432
2
    number_of_solutions: i32,
433
2
    capture_options: ConjureSolveCaptureOptions,
434
2
) -> Result<ConjureSolutions, anyhow::Error> {
435
    enum ConjureOutputDir {
436
        Temp(tempfile::TempDir),
437
        Fixed(PathBuf),
438
    }
439

            
440
    impl ConjureOutputDir {
441
6
        fn path(&self) -> &std::path::Path {
442
6
            match self {
443
6
                Self::Temp(dir) => dir.path(),
444
                Self::Fixed(path) => path,
445
            }
446
6
        }
447
    }
448

            
449
2
    let output_dir = match &capture_options.artifact_dir {
450
        Some(path) => {
451
            // A failed run leaves its diagnostics behind. Conjure may reuse files in an existing
452
            // output directory, and the glob below would also read stale solution files, so each
453
            // invocation must start with an empty artifact directory.
454
            if path.exists() {
455
                fs::remove_dir_all(path)?;
456
            }
457
            fs::create_dir_all(path)?;
458
            ConjureOutputDir::Fixed(path.clone())
459
        }
460
2
        None => ConjureOutputDir::Temp(tempdir()?),
461
    };
462

            
463
2
    let mut cmd = std::process::Command::new("conjure");
464
2
    let number_of_solutions_arg = if number_of_solutions == 0 {
465
2
        "all".to_string()
466
    } else {
467
        number_of_solutions.to_string()
468
    };
469

            
470
2
    cmd.arg("solve")
471
2
        .arg(format!("--number-of-solutions={number_of_solutions_arg}"))
472
2
        .arg("--copy-solutions=no")
473
2
        .arg("--solutions-in-one-file")
474
2
        .arg("--output-format=json")
475
2
        .arg("-o")
476
2
        .arg(output_dir.path());
477

            
478
2
    if let Some(options) = &capture_options.savilerow_options {
479
        cmd.arg(format!("--savilerow-options={options}"));
480
2
    }
481

            
482
2
    cmd.arg(essence_file);
483

            
484
2
    if let Some(file) = param_file {
485
2
        cmd.arg(file);
486
    }
487

            
488
2
    let conjure_solve_start = Instant::now();
489
2
    let output = cmd.output()?;
490
2
    let conjure_solve_wall_time_s = conjure_solve_start.elapsed().as_secs_f64();
491

            
492
2
    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
2
    }
500

            
501
2
    let domains = domains_for_conjure_solutions(essence_file, param_file, Arc::clone(&context))?;
502

            
503
    let solutions_files: Vec<_> =
504
2
        glob(&format!("{}/*.solutions.json", output_dir.path().display()))?.collect();
505
2
    if solutions_files.is_empty() {
506
        // Unsatisfiable / no solutions: Conjure may omit the solutions file.
507
        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
2
    }
513

            
514
2
    let mut solutions_set = Vec::new();
515
2
    for solutions_file in solutions_files {
516
2
        let solutions_file = solutions_file?;
517
2
        let text = fs::read_to_string(&solutions_file)?;
518
2
        solutions_set.extend(solutions_from_simplified_json_str(&text, &domains)?);
519
    }
520

            
521
2
    let timings = read_conjure_timings(output_dir.path(), conjure_solve_wall_time_s)?;
522

            
523
    Ok(ConjureSolutions {
524
2
        solutions: solutions_set
525
2
            .into_iter()
526
2
            .filter(|x| !x.is_empty())
527
            .collect(),
528
2
        timings,
529
    })
530
2
}
531

            
532
2
fn domains_for_conjure_solutions(
533
2
    essence_file: &str,
534
2
    param_file: Option<&str>,
535
2
    context: Arc<RwLock<Context<'static>>>,
536
2
) -> Result<BTreeMap<Name, conjure_cp::ast::DomainPtr>, anyhow::Error> {
537
2
    let problem = parse_essence_file_native(essence_file, Arc::clone(&context))?;
538
2
    let unified = match param_file {
539
2
        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
2
            let param_model = parse_essence_file_native(param_path, Arc::clone(&context))?;
549
2
            instantiate_model(problem, param_model)?
550
        }
551
        None => problem,
552
    };
553
2
    Ok(domains_from_model(&unified))
554
2
}
555

            
556
2
fn read_conjure_timings(
557
2
    path: &std::path::Path,
558
2
    conjure_solve_wall_time_s: f64,
559
2
) -> Result<Option<ConjureRunTimings>, anyhow::Error> {
560
2
    let stats_files: Vec<_> = glob(&format!("{}/*.stats.json", path.display()))?.collect();
561
2
    if stats_files.is_empty() {
562
        return Ok(None);
563
2
    }
564

            
565
2
    let mut timings = ConjureRunTimings {
566
2
        wall_clock_time_s: conjure_solve_wall_time_s,
567
2
        ..Default::default()
568
2
    };
569
2
    for stats_file in stats_files {
570
2
        let stats_file = stats_file?;
571
2
        let stats: JsonValue = serde_json::from_str(&fs::read_to_string(&stats_file)?)?;
572
2
        let savilerow_total_time = stats
573
2
            .pointer("/savilerowInfo/SavileRowTotalTime")
574
2
            .and_then(JsonValue::as_str)
575
2
            .and_then(|value| value.parse::<f64>().ok())
576
            .unwrap_or_default();
577
2
        let solve_time = stats
578
2
            .pointer("/savilerowInfo/SolverTotalTime")
579
2
            .and_then(JsonValue::as_str)
580
2
            .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
2
        timings.savilerow_translation_time_s += savilerow_total_time;
590
2
        timings.solve_time_s += solve_time;
591
    }
592

            
593
2
    timings.conjure_translation_time_s =
594
2
        (conjure_solve_wall_time_s - timings.savilerow_translation_time_s - timings.solve_time_s)
595
2
            .max(0.0);
596
2
    timings.translation_time_s =
597
2
        timings.conjure_translation_time_s + timings.savilerow_translation_time_s;
598

            
599
2
    Ok(Some(timings))
600
2
}
601

            
602
108
pub fn solutions_to_json(solutions: &[BTreeMap<Name, Literal>]) -> JsonValue {
603
108
    let json_solutions = solutions.iter().map(solution_to_json).collect();
604
108
    let ans = JsonValue::Array(json_solutions);
605
108
    sort_json_object(&ans, true)
606
108
}
607

            
608
/// Render solutions in the format produced by Conjure's `--solutions-in-one-file` option.
609
330789
pub fn solutions_to_essence(solutions: &[BTreeMap<Name, Literal>]) -> String {
610
330789
    let mut solutions = solutions.iter().collect::<Vec<_>>();
611
19475369
    solutions.sort_by(|lhs, rhs| solution_essence_cmp(lhs, rhs));
612

            
613
330789
    let mut output = String::new();
614
330789
    for (index, solution) in solutions.iter().enumerate() {
615
3846434
        writeln!(output, "$ Solution: {:06}", index + 1).unwrap();
616
3846434
        writeln!(output, "language Essence 1.3\n").unwrap();
617
3846434
        let mut unnamed_types = BTreeMap::new();
618
3846434
        for value in solution.values() {
619
12045402
            for literal in value.universe() {
620
21240820
                if let Literal::Unnamed(value) = literal {
621
4
                    unnamed_types.insert(value.ty.name.clone(), value.ty.size);
622
21240816
                }
623
            }
624
        }
625
3846434
        for (name, size) in unnamed_types {
626
8
            let members = (1..=size).map(|index| format!("{name}_{index}")).join(", ");
627
4
            writeln!(output, "letting {name} be new type enum {{{members}}}").unwrap();
628
        }
629
3846434
        for (name, value) in *solution {
630
12045402
            writeln!(output, "letting {name} be {value}").unwrap();
631
        }
632
3846434
        output.push_str("\n\n");
633
    }
634
330789
    output
635
330789
}
636

            
637
19475321
fn solution_essence_cmp(
638
19475321
    lhs: &BTreeMap<Name, Literal>,
639
19475321
    rhs: &BTreeMap<Name, Literal>,
640
19475321
) -> std::cmp::Ordering {
641
19475321
    lhs.iter()
642
19475321
        .zip(rhs)
643
35502271
        .find_map(|((lhs_name, lhs_value), (rhs_name, rhs_value))| {
644
35502271
            let ordering = lhs_name.cmp(rhs_name);
645
35502271
            (ordering != std::cmp::Ordering::Equal)
646
35502271
                .then_some(ordering)
647
35502271
                .or_else(|| {
648
35502271
                    let ordering = lhs_value.essence_cmp(rhs_value);
649
35502271
                    (ordering != std::cmp::Ordering::Equal).then_some(ordering)
650
35502271
                })
651
35502271
        })
652
26528
        .unwrap_or_else(|| lhs.len().cmp(&rhs.len()))
653
19475321
}
654

            
655
148
fn solution_to_json(solution: &BTreeMap<Name, Literal>) -> JsonValue {
656
148
    let mut json_solution = Map::new();
657
148
    for (var_name, constant) in solution {
658
434
        let serialized_constant = serde_json::to_value(constant).unwrap();
659
434
        json_solution.insert(var_name.to_string(), serialized_constant);
660
    }
661
148
    sort_json_object(&JsonValue::Object(json_solution), false)
662
148
}
663

            
664
#[cfg(test)]
665
mod tests {
666
    use super::*;
667
    use conjure_cp::ast::{DeclarationPtr, Domain, Moo, Reference};
668

            
669
    #[test]
670
1
    fn renders_conjure_multi_solution_essence_format() {
671
1
        let mut first = BTreeMap::new();
672
1
        first.insert(Name::User("b".into()), Literal::Bool(false));
673
1
        first.insert(Name::User("x".into()), Literal::Int(1));
674
1
        let mut second = BTreeMap::new();
675
1
        second.insert(Name::User("b".into()), Literal::Bool(true));
676
1
        second.insert(Name::User("x".into()), Literal::Int(2));
677

            
678
1
        assert_eq!(
679
1
            solutions_to_essence(&[first, second]),
680
1
            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
1
    }
692

            
693
    #[test]
694
1
    fn retroactive_pruning_removes_dominated_prior_solution() {
695
1
        let x = Name::User("x".into());
696
1
        let x_ref = Expression::Atomic(
697
1
            Metadata::new(),
698
1
            Atom::Reference(Reference::new(DeclarationPtr::new_find(
699
1
                x.clone(),
700
1
                Domain::bool(),
701
1
            ))),
702
1
        );
703
1
        let dominance_expression = Expression::Imply(
704
1
            Metadata::new(),
705
1
            Moo::new(x_ref),
706
1
            Moo::new(Expression::FromSolution(
707
1
                Metadata::new(),
708
1
                Moo::new(Atom::Reference(Reference::new(DeclarationPtr::new_find(
709
1
                    x.clone(),
710
1
                    Domain::bool(),
711
1
                )))),
712
1
            )),
713
1
        );
714

            
715
1
        let mut sol_true = BTreeMap::new();
716
1
        sol_true.insert(x.clone(), Literal::Int(1));
717
1
        let mut sol_false = BTreeMap::new();
718
1
        sol_false.insert(x, Literal::Int(0));
719

            
720
        let pruned =
721
1
            retroactively_prune_dominated(vec![sol_true, sol_false.clone()], &dominance_expression);
722

            
723
1
        assert_eq!(pruned, vec![sol_false]);
724
1
    }
725
}