Skip to main content

conjure_cp_core/solver/
mod.rs

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::solver::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_var("x".into(), Domain::Bool));
45//! model.as_submodel_mut().add_symbol(Declaration::new_var("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
104use std::any::Any;
105use std::cell::OnceCell;
106use std::collections::HashMap;
107use std::error::Error;
108use std::fmt::{Debug, Display};
109use std::io::Write;
110use std::rc::Rc;
111use std::str::FromStr;
112use std::sync::{Arc, RwLock};
113use std::time::Instant;
114
115use clap::ValueEnum;
116use schemars::JsonSchema;
117use serde::{Deserialize, Serialize};
118use strum_macros::{Display, EnumIter, EnumString};
119use thiserror::Error;
120
121use crate::Model;
122use crate::ast::{Literal, Name};
123use crate::context::Context;
124use crate::stats::SolverStats;
125
126#[cfg(feature = "smt")]
127use crate::solver::adaptors::smt::{IntTheory, MatrixTheory, TheoryConfig};
128
129use self::model_modifier::ModelModifier;
130use self::states::{ExecutionSuccess, Init, ModelLoaded, SolverState};
131
132pub mod adaptors;
133pub mod model_modifier;
134
135#[doc(hidden)]
136mod private;
137
138pub mod states;
139
140#[derive(
141    Debug, EnumIter, Display, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, JsonSchema,
142)]
143pub enum SolverFamily {
144    Sat,
145    Minion,
146    #[cfg(feature = "smt")]
147    Smt(TheoryConfig),
148}
149
150impl FromStr for SolverFamily {
151    type Err = String;
152
153    fn from_str(s: &str) -> Result<Self, Self::Err> {
154        let s = s.trim().to_ascii_lowercase();
155
156        match s.as_str() {
157            "minion" => Ok(SolverFamily::Minion),
158            "sat" => Ok(SolverFamily::Sat),
159            #[cfg(feature = "smt")]
160            "smt" => Ok(SolverFamily::Smt(TheoryConfig::default())),
161            other => {
162                // allow forms like `smt-bv-atomic` or `smt-lia-arrays`
163                #[cfg(feature = "smt")]
164                if other.starts_with("smt-") {
165                    let parts = other.split('-').skip(1);
166                    let mut ints = IntTheory::default();
167                    let mut matrices = MatrixTheory::default();
168                    let mut unwrap_alldiff = false;
169
170                    for token in parts {
171                        match token {
172                            "" => {}
173                            "lia" => ints = IntTheory::Lia,
174                            "bv" => ints = IntTheory::Bv,
175                            "arrays" => matrices = MatrixTheory::Arrays,
176                            "atomic" => matrices = MatrixTheory::Atomic,
177                            "nodiscrete" => unwrap_alldiff = true,
178                            other_token => {
179                                return Err(format!(
180                                    "unknown SMT theory option '{other_token}', must be one of bv|lia|arrays|atomic|nodiscrete"
181                                ));
182                            }
183                        }
184                    }
185
186                    return Ok(SolverFamily::Smt(TheoryConfig {
187                        ints,
188                        matrices,
189                        unwrap_alldiff,
190                    }));
191                }
192                Err(format!(
193                    "unknown solver family '{other}', expected 'minion', 'sat' or 'smt[(bv|lia)-(arrays|atomic)]'"
194                ))
195            }
196        }
197    }
198}
199
200/// The type for user-defined callbacks for use with [Solver].
201///
202/// Note that this enforces thread safety
203pub type SolverCallback = Box<dyn Fn(HashMap<Name, Literal>) -> bool + Send + Sync>;
204pub type SolverMutCallback =
205    Box<dyn Fn(HashMap<Name, Literal>, Box<dyn ModelModifier>) -> bool + Send + Sync>;
206
207/// A common interface for calling underlying solver APIs inside a [`Solver`].
208///
209/// Implementations of this trait aren't directly callable and should be used through [`Solver`] .
210///
211/// The below documentation lists the formal requirements that all implementations of
212/// [`SolverAdaptor`] should follow - **see the top level module documentation and [`Solver`] for
213/// usage details.**
214///
215/// # Encapsulation
216///
217///  The [`SolverAdaptor`] trait **must** only be implemented inside a submodule of this one,
218///  and **should** only be called through [`Solver`].
219///
220/// The `private::Sealed` trait and `private::Internal` type enforce these requirements by only
221/// allowing trait implementations and calling of methods of SolverAdaptor to occur inside this
222/// module.
223///
224/// # Thread Safety
225///
226/// Multiple instances of [`Solver`] can be run in parallel across multiple threads.
227///
228/// [`Solver`] provides no concurrency control or thread-safety; therefore, adaptors **must**
229/// ensure that multiple instances of themselves can be ran in parallel. This applies to all
230/// stages of solving including having two active `solve()` calls happening at a time, loading
231/// a model while another is mid-solve, loading two models at once, etc.
232///
233/// A [SolverAdaptor] **may** use whatever threading or process model it likes underneath the hood,
234/// as long as it obeys the above.
235///
236/// Method calls **should** block instead of erroring where possible.
237///
238/// Underlying solvers that only have one instance per process (such as Minion) **should** block
239/// (eg. using a [`Mutex<()>`](`std::sync::Mutex`)) to run calls to
240/// [`Solver<A,ModelLoaded>::solve()`] and [`Solver<A,ModelLoaded>::solve_mut()`] sequentially.
241pub trait SolverAdaptor: private::Sealed + Any {
242    /// Runs the solver on the given model.
243    ///
244    /// Implementations of this function **must** call the user provided callback whenever a solution
245    /// is found. If the user callback returns `true`, search should continue, if the user callback
246    /// returns `false`, search should terminate.
247    ///
248    /// # Returns
249    ///
250    /// If the solver terminates without crashing a [SolveSuccess] struct **must** returned. The
251    /// value of [SearchStatus] can be used to denote whether the underlying solver completed its
252    /// search or not. The latter case covers most non-crashing "failure" cases including user
253    /// termination, timeouts, etc.
254    ///
255    /// To help populate [SearchStatus], it may be helpful to implement counters that track if the
256    /// user callback has been called yet, and its return value. This information makes it is
257    /// possible to distinguish between the most common search statuses:
258    /// [SearchComplete::HasSolutions], [SearchComplete::NoSolutions], and
259    /// [SearchIncomplete::UserTerminated].
260    fn solve(
261        &mut self,
262        callback: SolverCallback,
263        _: private::Internal,
264    ) -> Result<SolveSuccess, SolverError>;
265
266    /// Runs the solver on the given model, allowing modification of the model through a
267    /// [`ModelModifier`].
268    ///
269    /// Implementations of this function **must** return [`OpNotSupported`](`ModificationFailure::OpNotSupported`)
270    /// if modifying the model mid-search is not supported.
271    ///
272    /// Otherwise, this should work in the same way as [`solve`](SolverAdaptor::solve).
273    fn solve_mut(
274        &mut self,
275        callback: SolverMutCallback,
276        _: private::Internal,
277    ) -> Result<SolveSuccess, SolverError>;
278    fn load_model(&mut self, model: Model, _: private::Internal) -> Result<(), SolverError>;
279    fn init_solver(&mut self, _: private::Internal) {}
280
281    /// Get the solver family that this solver adaptor belongs to
282    fn get_family(&self) -> SolverFamily;
283
284    /// Gets the name of the solver adaptor for pretty printing.
285    fn get_name(&self) -> &'static str;
286
287    /// Adds the solver adaptor name and family (if they exist) to the given stats object.
288    fn add_adaptor_info_to_stats(&self, stats: SolverStats) -> SolverStats {
289        SolverStats {
290            solver_adaptor: Some(String::from(self.get_name())),
291            solver_family: Some(self.get_family()),
292            ..stats
293        }
294    }
295
296    /// Writes a solver input file to the given writer.
297    ///
298    /// This method is for debugging use only, and there are no plans to make the solutions
299    /// obtained by running this file through the solver translatable back into high-level Essence.
300    ///
301    /// This file is runnable using the solvers command line interface. E.g. for Minion, this
302    /// outputs a valid .minion file.
303    ///
304    ///
305    /// # Implementation
306    /// + It can be helpful for this file to contain comments linking constraints and variables to
307    ///   their original essence, but this is not required.
308    ///
309    /// + This function is ran after model loading but before solving - therefore, it is safe for
310    ///   solving to mutate the model object.
311    fn write_solver_input_file(&self, writer: &mut Box<dyn Write>) -> Result<(), std::io::Error>;
312}
313
314/// An abstract representation of a constraints solver.
315///
316/// [Solver] provides a common interface for interacting with a constraint solver. It also
317/// abstracts over solver-specific datatypes, handling the translation to/from [conjure_cp_core::ast]
318/// types for a model and its solutions.
319///
320/// Details of how a model is solved is specified by the [SolverAdaptor]. This includes: the
321/// underlying solver used, the translation of the model to a solver compatible form, how solutions
322/// are translated back to [conjure_cp_core::ast] types, and how incremental solving is implemented.
323/// As such, there may be multiple [SolverAdaptor] implementations for a single underlying solver:
324/// e.g. one adaptor may give solutions in a representation close to the solvers, while another may
325/// attempt to rewrite it back into Essence.
326///
327pub struct Solver<State: SolverState = Init> {
328    state: State,
329    adaptor: Box<dyn SolverAdaptor>,
330    context: Option<Arc<RwLock<Context<'static>>>>,
331}
332
333impl Solver {
334    pub fn new<A: SolverAdaptor>(solver_adaptor: A) -> Solver {
335        let mut solver = Solver {
336            state: Init,
337            adaptor: Box::new(solver_adaptor),
338            context: None,
339        };
340
341        solver.adaptor.init_solver(private::Internal);
342        solver
343    }
344
345    pub fn get_family(&self) -> SolverFamily {
346        self.adaptor.get_family()
347    }
348
349    pub fn get_name(&self) -> &'static str {
350        self.adaptor.get_name()
351    }
352}
353
354impl Solver<Init> {
355    pub fn load_model(mut self, model: Model) -> Result<Solver<ModelLoaded>, SolverError> {
356        let solver_model = &mut self.adaptor.load_model(model.clone(), private::Internal)?;
357        Ok(Solver {
358            state: ModelLoaded,
359            adaptor: self.adaptor,
360            context: Some(model.context.clone()),
361        })
362    }
363}
364
365impl Solver<ModelLoaded> {
366    pub fn solve(
367        mut self,
368        callback: SolverCallback,
369    ) -> Result<Solver<ExecutionSuccess>, SolverError> {
370        #[allow(clippy::unwrap_used)]
371        let start_time = Instant::now();
372
373        #[allow(clippy::unwrap_used)]
374        let result = self.adaptor.solve(callback, private::Internal);
375
376        let duration = start_time.elapsed();
377
378        match result {
379            Ok(x) => {
380                let stats = self
381                    .adaptor
382                    .add_adaptor_info_to_stats(x.stats)
383                    .with_timings(duration.as_secs_f64());
384
385                Ok(Solver {
386                    adaptor: self.adaptor,
387                    state: ExecutionSuccess {
388                        stats,
389                        status: x.status,
390                        _sealed: private::Internal,
391                    },
392                    context: self.context,
393                })
394            }
395            Err(x) => Err(x),
396        }
397    }
398
399    pub fn solve_mut(
400        mut self,
401        callback: SolverMutCallback,
402    ) -> Result<Solver<ExecutionSuccess>, SolverError> {
403        #[allow(clippy::unwrap_used)]
404        let start_time = Instant::now();
405
406        #[allow(clippy::unwrap_used)]
407        let result = self.adaptor.solve_mut(callback, private::Internal);
408
409        let duration = start_time.elapsed();
410
411        match result {
412            Ok(x) => {
413                let stats = self
414                    .adaptor
415                    .add_adaptor_info_to_stats(x.stats)
416                    .with_timings(duration.as_secs_f64());
417
418                Ok(Solver {
419                    adaptor: self.adaptor,
420                    state: ExecutionSuccess {
421                        stats,
422                        status: x.status,
423                        _sealed: private::Internal,
424                    },
425                    context: self.context,
426                })
427            }
428            Err(x) => Err(x),
429        }
430    }
431
432    /// Writes a solver input file to the given writer.
433    ///
434    /// This method is for debugging use only, and there are no plans to make the solutions
435    /// obtained by running this file through the solver translatable back into high-level Essence.
436    ///
437    /// This file is runnable using the solvers command line interface. E.g. for Minion, this
438    /// outputs a valid .minion file.
439    ///
440    /// This function is only available in the `ModelLoaded` state as solvers are allowed to edit
441    /// the model in place.
442    pub fn write_solver_input_file(
443        &self,
444        writer: &mut Box<dyn Write>,
445    ) -> Result<(), std::io::Error> {
446        self.adaptor.write_solver_input_file(writer)
447    }
448}
449
450impl Solver<ExecutionSuccess> {
451    pub fn stats(&self) -> SolverStats {
452        self.state.stats.clone()
453    }
454
455    // Saves this solvers stats to the global context as a "solver run"
456    pub fn save_stats_to_context(&self) {
457        #[allow(clippy::unwrap_used)]
458        #[allow(clippy::expect_used)]
459        self.context
460            .as_ref()
461            .expect("")
462            .write()
463            .unwrap()
464            .stats
465            .add_solver_run(self.stats());
466    }
467
468    pub fn wall_time_s(&self) -> f64 {
469        self.stats().conjure_solver_wall_time_s
470    }
471}
472
473/// Errors returned by [Solver] on failure.
474#[non_exhaustive]
475#[derive(Debug, Error, Clone)]
476pub enum SolverError {
477    #[error("operation not implemented yet: {0}")]
478    OpNotImplemented(String),
479
480    #[error("operation not supported: {0}")]
481    OpNotSupported(String),
482
483    #[error("model feature not supported: {0}")]
484    ModelFeatureNotSupported(String),
485
486    #[error("model feature not implemented yet: {0}")]
487    ModelFeatureNotImplemented(String),
488
489    // use for semantics / type errors, use the above for syntax
490    #[error("model invalid: {0}")]
491    ModelInvalid(String),
492
493    #[error("error during solver execution: not implemented: {0}")]
494    RuntimeNotImplemented(String),
495
496    #[error("error during solver execution: {0}")]
497    Runtime(String),
498}
499
500pub type SolverResult<T> = Result<T, SolverError>;
501
502/// Returned from [SolverAdaptor] when solving is successful.
503pub struct SolveSuccess {
504    stats: SolverStats,
505    status: SearchStatus,
506}
507
508pub enum SearchStatus {
509    /// The search was complete (i.e. the solver found all possible solutions)
510    Complete(SearchComplete),
511    /// The search was incomplete (i.e. it was terminated before all solutions were found)
512    Incomplete(SearchIncomplete),
513}
514
515#[non_exhaustive]
516pub enum SearchIncomplete {
517    Timeout,
518    UserTerminated,
519    #[doc(hidden)]
520    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
521    __NonExhaustive,
522}
523
524#[non_exhaustive]
525pub enum SearchComplete {
526    HasSolutions,
527    NoSolutions,
528    #[doc(hidden)]
529    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
530    __NonExhaustive,
531}