1
use std::collections::HashMap;
2
use std::io::Read;
3
use std::path::Path;
4
use std::string::ToString;
5
use std::sync::{Arc, Mutex, RwLock};
6

            
7
use conjure_core::ast::{Literal, Name};
8
use conjure_core::context::Context;
9
use rand::Rng as _;
10
use serde_json::{from_str, Map, Value as JsonValue};
11
use serde::Deserialize;
12
use thiserror::Error as ThisError;
13

            
14
use std::fs::File;
15

            
16
use crate::model_from_json;
17
use crate::solver::adaptors::Minion;
18
use crate::solver::Solver;
19
use crate::utils::json::sort_json_object;
20
use crate::Error as ParseErr;
21
use crate::Model;
22

            
23
use glob::glob;
24

            
25
#[derive(Debug, ThisError)]
26
pub enum EssenceParseError {
27
    #[error("Error running conjure pretty: {0}")]
28
    ConjurePrettyError(String),
29
    #[error("Error running conjure solve: {0}")]
30
    ConjureSolveError(String),
31
    #[error("Error parsing essence file: {0}")]
32
    ParseError(ParseErr),
33
    #[error("Error parsing Conjure solutions file: {0}")]
34
    ConjureSolutionsError(String),
35
    #[error("No solutions file for {0}")]
36
    ConjureNoSolutionsFile(String),
37
}
38

            
39
impl From<ParseErr> for EssenceParseError {
40
    fn from(e: ParseErr) -> Self {
41
        EssenceParseError::ParseError(e)
42
    }
43
}
44

            
45
300
pub fn parse_essence_file(
46
300
    path: &str,
47
300
    filename: &str,
48
300
    extension: &str,
49
300
    context: Arc<RwLock<Context<'static>>>,
50
300
) -> Result<Model, EssenceParseError> {
51
300
    let mut cmd = std::process::Command::new("conjure");
52
300
    let output = match cmd
53
300
        .arg("pretty")
54
300
        .arg("--output-format=astjson")
55
300
        .arg(format!("{path}/{filename}.{extension}"))
56
300
        .output()
57
    {
58
300
        Ok(output) => output,
59
        Err(e) => return Err(EssenceParseError::ConjurePrettyError(e.to_string())),
60
    };
61

            
62
300
    if !output.status.success() {
63
        let stderr_string = String::from_utf8(output.stderr)
64
            .unwrap_or("stderr is not a valid UTF-8 string".to_string());
65
        return Err(EssenceParseError::ConjurePrettyError(stderr_string));
66
300
    }
67

            
68
300
    let astjson = match String::from_utf8(output.stdout) {
69
300
        Ok(astjson) => astjson,
70
        Err(e) => {
71
            return Err(EssenceParseError::ConjurePrettyError(format!(
72
                "Error parsing output from conjure: {:#?}",
73
                e
74
            )))
75
        }
76
    };
77

            
78
300
    let parsed_model = model_from_json(&astjson, context)?;
79
300
    Ok(parsed_model)
80
300
}
81

            
82
300
pub fn get_minion_solutions(model: Model) -> Result<Vec<HashMap<Name, Literal>>, anyhow::Error> {
83
300
    let solver = Solver::new(Minion::new());
84
300

            
85
300
    println!("Building Minion model...");
86
300
    let solver = solver.load_model(model)?;
87

            
88
300
    println!("Running Minion...");
89
300

            
90
300
    let all_solutions_ref = Arc::new(Mutex::<Vec<HashMap<Name, Literal>>>::new(vec![]));
91
300
    let all_solutions_ref_2 = all_solutions_ref.clone();
92
300
    #[allow(clippy::unwrap_used)]
93
300
    let solver = solver
94
9378
        .solve(Box::new(move |sols| {
95
9378
            let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
96
9378
            (*all_solutions).push(sols);
97
9378
            true
98
9378
        }))
99
300
        .unwrap();
100
300

            
101
300
    solver.save_stats_to_context();
102
300

            
103
300
    #[allow(clippy::unwrap_used)]
104
300
    let sols = (*all_solutions_ref).lock().unwrap();
105
300

            
106
300
    Ok((*sols).clone())
107
300
}
108

            
109
//struct(s) for stats.json. contain only relevant information, serde will skip any objects not named
110
#[derive(Deserialize)]
111
pub struct PerformMetric {
112
    #[serde(rename = "SavileRowInfo")]
113
    savile_row_info: SavileRowInfo,
114
    status: String,
115
}
116

            
117
#[derive(Deserialize)]
118
struct SavileRowInfo {
119
    #[serde(rename = "SavileRowTotalTime")]
120
    savile_row_total_time: f32,
121
    #[serde(rename = "SolverNodes")]
122
    solver_nodes: u32,
123
    #[serde(rename = "SolverTotalTime")]
124
    solver_total_time: f32,
125
}
126

            
127
#[allow(clippy::unwrap_used)]
128
pub fn get_solutions_from_conjure(
129
    essence_file: &str,
130
) -> Result<(Vec<HashMap<Name, Literal>>, PerformMetric), EssenceParseError> {
131
    // this is ran in parallel, and we have no guarantee by rust that invocations to this function
132
    // don't share the same tmp dir.
133
    let mut rng = rand::thread_rng();
134
    let rand: i8 = rng.gen();
135

            
136
    let mut tmp_dir = std::env::temp_dir();
137
    tmp_dir.push(Path::new(&rand.to_string()));
138

            
139
    let mut cmd = std::process::Command::new("conjure");
140
    let output = cmd
141
        .arg("solve")
142
        .arg("--output-format=json")
143
        .arg("--solutions-in-one-file")
144
        .arg("--number-of-solutions=all")
145
        .arg("--copy-solutions=no")
146
        .arg("-o")
147
        .arg(&tmp_dir)
148
        .arg(essence_file)
149
        .output()
150
        .map_err(|e| EssenceParseError::ConjureSolveError(e.to_string()))?;
151

            
152
    if !output.status.success() {
153
        return Err(EssenceParseError::ConjureSolveError(format!(
154
            "conjure solve exited with failure: {}",
155
            String::from_utf8(output.stderr).unwrap()
156
        )));
157
    }
158

            
159
    let solutions_files: Vec<_> = glob(&format!("{}/*.solutions.json", tmp_dir.display()))
160
        .unwrap()
161
        .collect();
162

            
163
    if solutions_files.is_empty() {
164
        return Err(EssenceParseError::ConjureNoSolutionsFile(
165
            tmp_dir.display().to_string(),
166
        ));
167
    }
168

            
169
    let solutions_file = solutions_files[0].as_ref().unwrap();
170
    let mut file = File::open(solutions_file).unwrap();
171

            
172
    let mut json_str = String::new();
173
    file.read_to_string(&mut json_str).unwrap();
174
    let mut json: JsonValue =
175
        from_str(&json_str).map_err(|e| EssenceParseError::ConjureSolutionsError(e.to_string()))?;
176
    json.sort_all_objects();
177

            
178
    //object for stats file
179
    let mut stats_file = File::open("model000001.solutions.json") 
180
        .unwrap();
181

            
182
    //read to string and populate PerformanceMetric
183
    let mut stats_json_str = String::new();
184
    stats_file.read_to_string(&mut stats_json_str).unwrap();
185
    let stats: PerformMetric = serde_json::from_str(&stats_json_str).unwrap();
186

            
187
    let solutions = json
188
        .as_array()
189
        .ok_or(EssenceParseError::ConjureSolutionsError(
190
            "expected solutions to be an array".to_owned(),
191
        ))?;
192

            
193
    let mut solutions_set: Vec<HashMap<Name, Literal>> = Vec::new();
194

            
195
    for solution in solutions {
196
        let mut solution_map = HashMap::new();
197
        let solution = solution
198
            .as_object()
199
            .ok_or(EssenceParseError::ConjureSolutionsError(
200
                "invalid json".to_owned(),
201
            ))?;
202
        for (name, value) in solution {
203
            let name = Name::UserName(name.to_owned());
204
            let value = match value {
205
                JsonValue::Bool(b) => Ok(Literal::Bool(*b)),
206
                JsonValue::Number(n) => Ok(Literal::Int(n.as_i64().unwrap().try_into().unwrap())),
207
                a => Err(EssenceParseError::ConjureSolutionsError(
208
                    format!("expected constant, got {}", a).to_owned(),
209
                )),
210
            }?;
211
            solution_map.insert(name, value);
212
        }
213
        solutions_set.push(solution_map);
214
    }
215

            
216
    Ok((solutions_set, stats))
217
}
218

            
219
300
pub fn minion_solutions_to_json(solutions: &Vec<HashMap<Name, Literal>>) -> JsonValue {
220
300
    let mut json_solutions = Vec::new();
221
9678
    for solution in solutions {
222
9378
        let mut json_solution = Map::new();
223
51810
        for (var_name, constant) in solution {
224
42432
            let serialized_constant = match constant {
225
42432
                Literal::Int(i) => JsonValue::Number((*i).into()),
226
                Literal::Bool(b) => JsonValue::Bool(*b),
227
            };
228
42432
            json_solution.insert(var_name.to_string(), serialized_constant);
229
        }
230
9378
        json_solutions.push(JsonValue::Object(json_solution));
231
    }
232
300
    let ans = JsonValue::Array(json_solutions);
233
300
    sort_json_object(&ans, true)
234
300
}