1
//! A high-level API for interacting with constraints solvers.
2
//!
3
//! This module provides a consistent, solver-independent API for interacting with constraints
4
//! solvers. It also provides incremental solving support, and the returning of run stats from
5
//! solvers.
6
//!
7
//! -----
8
//!
9
//! - [Solver<Adaptor>] provides the API for interacting with constraints solvers.
10
//!
11
//! - The [SolverAdaptor] trait controls how solving actually occurs and handles translation
12
//!   between the [Solver] type and a specific solver.
13
//!
14
//! - [adaptors] contains all implemented solver adaptors.
15
//!
16
//! - The [model_modifier] submodule defines types to help with incremental solving / changing a
17
//!   model during search. The entrypoint for incremental solving is the [Solver<A,ModelLoaded>::solve_mut]
18
//!   function.
19
//!
20
//! # Examples
21
//!
22
//! ## A Successful Minion Model
23
//!
24
//! Note: this example constructs a basic Minion-compatible model instead of using the rewriter.
25
//! For a full end-to-end example, see crates/conjure-cp/examples/solver_hello_minion.rs
26
//!
27
//! ```ignore
28
//! use std::sync::{Arc,Mutex};
29
//! use conjure_cp_core::parse::get_example_model;
30
//! use conjure_cp_core::rule_engine::resolve_rule_sets;
31
//! use conjure_cp_core::rule_engine::rewrite_naive;
32
//! use conjure_cp_core::solver::{adaptors, Solver, SolverAdaptor};
33
//! use conjure_cp_core::solver::states::ModelLoaded;
34
//! use conjure_cp_core::Model;
35
//! use conjure_cp_core::ast::Domain;
36
//! use conjure_cp_core::ast::Declaration;
37
//! use conjure_cp_core::settings::SolverFamily;
38
//! use conjure_cp_core::context::Context;
39
//! use conjure_cp_essence_macros::essence_expr;
40
//!
41
//! // Define a model for minion.
42
//! let context = Context::<'static>::new_ptr_empty(SolverFamily::Minion);
43
//! let mut model = Model::new(context);
44
//! model.as_submodel_mut().add_symbol(Declaration::new_find("x".into(), Domain::Bool));
45
//! model.as_submodel_mut().add_symbol(Declaration::new_find("y".into(), Domain::Bool));
46
//! model.as_submodel_mut().add_constraint(essence_expr!{x != y});
47
//!
48
//! // Solve using Minion.
49
//! let solver = Solver::new(adaptors::Minion::new());
50
//! let solver: Solver<adaptors::Minion,ModelLoaded> = solver.load_model(model).unwrap();
51
//!
52
//! // In this example, we will count solutions.
53
//! //
54
//! // The solver interface is designed to allow adaptors to use multiple-threads / processes if
55
//! // necessary. Therefore, the callback type requires all variables inside it to have a static
56
//! // lifetime and to implement Send (i.e. the variable can be safely shared between theads).
57
//! //
58
//! // We use Arc<Mutex<T>> to create multiple references to a threadsafe mutable
59
//! // variable of type T.
60
//! //
61
//! // Using the move |x| ... closure syntax, we move one of these references into the closure.
62
//! // Note that a normal closure borrow variables from the parent so is not
63
//! // thread-safe.
64
//!
65
//! let counter_ref = Arc::new(Mutex::new(0));
66
//! let counter_ref_2 = counter_ref.clone();
67
//! solver.solve(Box::new(move |_| {
68
//!   let mut counter = (*counter_ref_2).lock().unwrap();
69
//!   *counter += 1;
70
//!   true
71
//!   }));
72
//!
73
//! let mut counter = (*counter_ref).lock().unwrap();
74
//! assert_eq!(*counter,2);
75
//! ```
76
//!
77
//! # The Solver callback function
78
//!
79
//! The callback function given to `solve` is called whenever a solution is found by the solver.
80
//!
81
//! Its return value can be used to control how many solutions the solver finds:
82
//!
83
//! * If the callback function returns `true`, solver execution continues.
84
//! * If the callback function returns `false`, the solver is terminated.
85
//!
86

            
87
// # Implementing Solver interfaces
88
//
89
// Solver interfaces can only be implemented inside this module, due to the SolverAdaptor crate
90
// being sealed.
91
//
92
// To add support for a solver, implement the `SolverAdaptor` trait in a submodule.
93
//
94
// If incremental solving support is required, also implement a new `ModelModifier`. If this is not
95
// required, all `ModelModifier` instances required by the SolverAdaptor trait can be replaced with
96
// NotModifiable.
97
//
98
// For more details, see the docstrings for SolverAdaptor, ModelModifier, and NotModifiable.
99

            
100
#![allow(dead_code)]
101
#![allow(unused)]
102
#![allow(clippy::manual_non_exhaustive)]
103

            
104
use std::any::Any;
105
use std::cell::OnceCell;
106
use std::collections::HashMap;
107
use std::error::Error;
108
use std::fmt::{Debug, Display};
109
use std::io::Write;
110
use std::rc::Rc;
111
use std::sync::{Arc, RwLock};
112
use std::time::Instant;
113

            
114
use clap::ValueEnum;
115
use thiserror::Error;
116

            
117
use crate::Model;
118
use crate::ast::{Literal, Name};
119
use crate::context::Context;
120
use crate::settings::SolverFamily;
121
use crate::stats::SolverStats;
122

            
123
use self::model_modifier::ModelModifier;
124
use self::states::{ExecutionSuccess, Init, ModelLoaded, SolverState};
125

            
126
pub mod adaptors;
127
pub mod model_modifier;
128

            
129
#[doc(hidden)]
130
mod private;
131

            
132
pub mod states;
133

            
134
/// The type for user-defined callbacks for use with [Solver].
135
///
136
/// Note that this enforces thread safety
137
pub type SolverCallback = Box<dyn Fn(HashMap<Name, Literal>) -> bool + Send + Sync>;
138
pub type SolverMutCallback =
139
    Box<dyn Fn(HashMap<Name, Literal>, Box<dyn ModelModifier>) -> bool + Send + Sync>;
140

            
141
/// A common interface for calling underlying solver APIs inside a [`Solver`].
142
///
143
/// Implementations of this trait aren't directly callable and should be used through [`Solver`] .
144
///
145
/// The below documentation lists the formal requirements that all implementations of
146
/// [`SolverAdaptor`] should follow - **see the top level module documentation and [`Solver`] for
147
/// usage details.**
148
///
149
/// # Encapsulation
150
///
151
///  The [`SolverAdaptor`] trait **must** only be implemented inside a submodule of this one,
152
///  and **should** only be called through [`Solver`].
153
///
154
/// The `private::Sealed` trait and `private::Internal` type enforce these requirements by only
155
/// allowing trait implementations and calling of methods of SolverAdaptor to occur inside this
156
/// module.
157
///
158
/// # Thread Safety
159
///
160
/// Multiple instances of [`Solver`] can be run in parallel across multiple threads.
161
///
162
/// [`Solver`] provides no concurrency control or thread-safety; therefore, adaptors **must**
163
/// ensure that multiple instances of themselves can be ran in parallel. This applies to all
164
/// stages of solving including having two active `solve()` calls happening at a time, loading
165
/// a model while another is mid-solve, loading two models at once, etc.
166
///
167
/// A [SolverAdaptor] **may** use whatever threading or process model it likes underneath the hood,
168
/// as long as it obeys the above.
169
///
170
/// Method calls **should** block instead of erroring where possible.
171
///
172
/// Underlying solvers that only have one instance per process (such as Minion) **should** block
173
/// (eg. using a [`Mutex<()>`](`std::sync::Mutex`)) to run calls to
174
/// [`Solver<A,ModelLoaded>::solve()`] and [`Solver<A,ModelLoaded>::solve_mut()`] sequentially.
175
pub trait SolverAdaptor: private::Sealed + Any {
176
    /// Runs the solver on the given model.
177
    ///
178
    /// Implementations of this function **must** call the user provided callback whenever a solution
179
    /// is found. If the user callback returns `true`, search should continue, if the user callback
180
    /// returns `false`, search should terminate.
181
    ///
182
    /// # Returns
183
    ///
184
    /// If the solver terminates without crashing a [SolveSuccess] struct **must** returned. The
185
    /// value of [SearchStatus] can be used to denote whether the underlying solver completed its
186
    /// search or not. The latter case covers most non-crashing "failure" cases including user
187
    /// termination, timeouts, etc.
188
    ///
189
    /// To help populate [SearchStatus], it may be helpful to implement counters that track if the
190
    /// user callback has been called yet, and its return value. This information makes it is
191
    /// possible to distinguish between the most common search statuses:
192
    /// [SearchComplete::HasSolutions], [SearchComplete::NoSolutions], and
193
    /// [SearchIncomplete::UserTerminated].
194
    fn solve(
195
        &mut self,
196
        callback: SolverCallback,
197
        _: private::Internal,
198
    ) -> Result<SolveSuccess, SolverError>;
199

            
200
    /// Runs the solver on the given model, allowing modification of the model through a
201
    /// [`ModelModifier`].
202
    ///
203
    /// Implementations of this function **must** return [`OpNotSupported`](`ModificationFailure::OpNotSupported`)
204
    /// if modifying the model mid-search is not supported.
205
    ///
206
    /// Otherwise, this should work in the same way as [`solve`](SolverAdaptor::solve).
207
    fn solve_mut(
208
        &mut self,
209
        callback: SolverMutCallback,
210
        _: private::Internal,
211
    ) -> Result<SolveSuccess, SolverError>;
212
    fn load_model(&mut self, model: Model, _: private::Internal) -> Result<(), SolverError>;
213
886
    fn init_solver(&mut self, _: private::Internal) {}
214

            
215
    /// Get the solver family that this solver adaptor belongs to
216
    fn get_family(&self) -> SolverFamily;
217

            
218
    /// Gets the name of the solver adaptor for pretty printing.
219
    fn get_name(&self) -> &'static str;
220

            
221
    /// Adds the solver adaptor name and family (if they exist) to the given stats object.
222
883
    fn add_adaptor_info_to_stats(&self, stats: SolverStats) -> SolverStats {
223
883
        SolverStats {
224
883
            solver_adaptor: Some(String::from(self.get_name())),
225
883
            solver_family: Some(self.get_family()),
226
883
            ..stats
227
883
        }
228
883
    }
229

            
230
    /// Writes a solver input file to the given writer.
231
    ///
232
    /// This method is for debugging use only, and there are no plans to make the solutions
233
    /// obtained by running this file through the solver translatable back into high-level Essence.
234
    ///
235
    /// This file is runnable using the solvers command line interface. E.g. for Minion, this
236
    /// outputs a valid .minion file.
237
    ///
238
    ///
239
    /// # Implementation
240
    /// + It can be helpful for this file to contain comments linking constraints and variables to
241
    ///   their original essence, but this is not required.
242
    ///
243
    /// + This function is ran after model loading but before solving - therefore, it is safe for
244
    ///   solving to mutate the model object.
245
    fn write_solver_input_file(&self, writer: &mut Box<dyn Write>) -> Result<(), std::io::Error>;
246
}
247

            
248
/// An abstract representation of a constraints solver.
249
///
250
/// [Solver] provides a common interface for interacting with a constraint solver. It also
251
/// abstracts over solver-specific datatypes, handling the translation to/from [conjure_cp_core::ast]
252
/// types for a model and its solutions.
253
///
254
/// Details of how a model is solved is specified by the [SolverAdaptor]. This includes: the
255
/// underlying solver used, the translation of the model to a solver compatible form, how solutions
256
/// are translated back to [conjure_cp_core::ast] types, and how incremental solving is implemented.
257
/// As such, there may be multiple [SolverAdaptor] implementations for a single underlying solver:
258
/// e.g. one adaptor may give solutions in a representation close to the solvers, while another may
259
/// attempt to rewrite it back into Essence.
260
///
261
pub struct Solver<State: SolverState = Init> {
262
    state: State,
263
    adaptor: Box<dyn SolverAdaptor>,
264
    context: Option<Arc<RwLock<Context<'static>>>>,
265
}
266

            
267
impl Solver {
268
886
    pub fn new<A: SolverAdaptor>(solver_adaptor: A) -> Solver {
269
886
        let mut solver = Solver {
270
886
            state: Init,
271
886
            adaptor: Box::new(solver_adaptor),
272
886
            context: None,
273
886
        };
274

            
275
886
        solver.adaptor.init_solver(private::Internal);
276
886
        solver
277
886
    }
278

            
279
    pub fn get_family(&self) -> SolverFamily {
280
        self.adaptor.get_family()
281
    }
282

            
283
5590
    pub fn get_name(&self) -> &'static str {
284
5590
        self.adaptor.get_name()
285
5590
    }
286
}
287

            
288
impl Solver<Init> {
289
6450
    pub fn load_model(mut self, model: Model) -> Result<Solver<ModelLoaded>, SolverError> {
290
6450
        let solver_model = &mut self.adaptor.load_model(model.clone(), private::Internal)?;
291
6430
        Ok(Solver {
292
6430
            state: ModelLoaded,
293
6430
            adaptor: self.adaptor,
294
6430
            context: Some(model.context.clone()),
295
6430
        })
296
6450
    }
297
}
298

            
299
impl Solver<ModelLoaded> {
300
6430
    pub fn solve(
301
6430
        mut self,
302
6430
        callback: SolverCallback,
303
6430
    ) -> Result<Solver<ExecutionSuccess>, SolverError> {
304
        #[allow(clippy::unwrap_used)]
305
6430
        let start_time = Instant::now();
306

            
307
        #[allow(clippy::unwrap_used)]
308
6430
        let result = self.adaptor.solve(callback, private::Internal);
309

            
310
6430
        let duration = start_time.elapsed();
311

            
312
6430
        match result {
313
6430
            Ok(x) => {
314
6430
                let stats = self
315
6430
                    .adaptor
316
6430
                    .add_adaptor_info_to_stats(x.stats)
317
6430
                    .with_timings(duration.as_secs_f64());
318

            
319
6430
                Ok(Solver {
320
6430
                    adaptor: self.adaptor,
321
6430
                    state: ExecutionSuccess {
322
6430
                        stats,
323
6430
                        status: x.status,
324
6430
                        _sealed: private::Internal,
325
6430
                    },
326
6430
                    context: self.context,
327
6430
                })
328
            }
329
            Err(x) => Err(x),
330
        }
331
6430
    }
332

            
333
    pub fn solve_mut(
334
        mut self,
335
        callback: SolverMutCallback,
336
    ) -> Result<Solver<ExecutionSuccess>, SolverError> {
337
        #[allow(clippy::unwrap_used)]
338
        let start_time = Instant::now();
339

            
340
        #[allow(clippy::unwrap_used)]
341
        let result = self.adaptor.solve_mut(callback, private::Internal);
342

            
343
        let duration = start_time.elapsed();
344

            
345
        match result {
346
            Ok(x) => {
347
                let stats = self
348
                    .adaptor
349
                    .add_adaptor_info_to_stats(x.stats)
350
                    .with_timings(duration.as_secs_f64());
351

            
352
                Ok(Solver {
353
                    adaptor: self.adaptor,
354
                    state: ExecutionSuccess {
355
                        stats,
356
                        status: x.status,
357
                        _sealed: private::Internal,
358
                    },
359
                    context: self.context,
360
                })
361
            }
362
            Err(x) => Err(x),
363
        }
364
    }
365

            
366
    /// Writes a solver input file to the given writer.
367
    ///
368
    /// This method is for debugging use only, and there are no plans to make the solutions
369
    /// obtained by running this file through the solver translatable back into high-level Essence.
370
    ///
371
    /// This file is runnable using the solvers command line interface. E.g. for Minion, this
372
    /// outputs a valid .minion file.
373
    ///
374
    /// This function is only available in the `ModelLoaded` state as solvers are allowed to edit
375
    /// the model in place.
376
    pub fn write_solver_input_file(
377
        &self,
378
        writer: &mut Box<dyn Write>,
379
    ) -> Result<(), std::io::Error> {
380
        self.adaptor.write_solver_input_file(writer)
381
    }
382
}
383

            
384
impl Solver<ExecutionSuccess> {
385
5590
    pub fn stats(&self) -> SolverStats {
386
5590
        self.state.stats.clone()
387
5590
    }
388

            
389
    // Saves this solvers stats to the global context as a "solver run"
390
5590
    pub fn save_stats_to_context(&self) {
391
        #[allow(clippy::unwrap_used)]
392
        #[allow(clippy::expect_used)]
393
5590
        self.context
394
5590
            .as_ref()
395
5590
            .expect("")
396
5590
            .write()
397
5590
            .unwrap()
398
5590
            .stats
399
5590
            .add_solver_run(self.stats());
400
5590
    }
401

            
402
    pub fn wall_time_s(&self) -> f64 {
403
        self.stats().conjure_solver_wall_time_s
404
    }
405
}
406

            
407
/// Errors returned by [Solver] on failure.
408
#[non_exhaustive]
409
#[derive(Debug, Error, Clone)]
410
pub enum SolverError {
411
    #[error("operation not implemented yet: {0}")]
412
    OpNotImplemented(String),
413

            
414
    #[error("operation not supported: {0}")]
415
    OpNotSupported(String),
416

            
417
    #[error("model feature not supported: {0}")]
418
    ModelFeatureNotSupported(String),
419

            
420
    #[error("model feature not implemented yet: {0}")]
421
    ModelFeatureNotImplemented(String),
422

            
423
    // use for semantics / type errors, use the above for syntax
424
    #[error("model invalid: {0}")]
425
    ModelInvalid(String),
426

            
427
    #[error("error during solver execution: not implemented: {0}")]
428
    RuntimeNotImplemented(String),
429

            
430
    #[error("error during solver execution: {0}")]
431
    Runtime(String),
432
}
433

            
434
pub type SolverResult<T> = Result<T, SolverError>;
435

            
436
/// Returned from [SolverAdaptor] when solving is successful.
437
pub struct SolveSuccess {
438
    stats: SolverStats,
439
    status: SearchStatus,
440
}
441

            
442
pub enum SearchStatus {
443
    /// The search was complete (i.e. the solver found all possible solutions)
444
    Complete(SearchComplete),
445
    /// The search was incomplete (i.e. it was terminated before all solutions were found)
446
    Incomplete(SearchIncomplete),
447
}
448

            
449
#[non_exhaustive]
450
pub enum SearchIncomplete {
451
    Timeout,
452
    UserTerminated,
453
    #[doc(hidden)]
454
    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
455
    __NonExhaustive,
456
}
457

            
458
#[non_exhaustive]
459
pub enum SearchComplete {
460
    HasSolutions,
461
    NoSolutions,
462
    #[doc(hidden)]
463
    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
464
    __NonExhaustive,
465
}