conjure_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//! ```rust
25//! use std::sync::{Arc,Mutex};
26//! use conjure_core::parse::get_example_model;
27//! use conjure_core::rule_engine::resolve_rule_sets;
28//! use conjure_core::rule_engine::rewrite_model;
29//! use conjure_core::solver::{adaptors, Solver, SolverAdaptor};
30//! use conjure_core::solver::states::ModelLoaded;
31//! use conjure_core::solver::SolverFamily;
32//!
33//! // Define and rewrite a model for minion.
34//! let model = get_example_model("bool-03").unwrap();
35//! let rule_sets = resolve_rule_sets(SolverFamily::Minion, &vec!["Constant"]).unwrap();
36//! let model = rewrite_model(&model,&rule_sets).unwrap();
37//!
38//!
39//! // Solve using Minion.
40//! let solver = Solver::new(adaptors::Minion::new());
41//! let solver: Solver<adaptors::Minion,ModelLoaded> = solver.load_model(model).unwrap();
42//!
43//! // In this example, we will count solutions.
44//! //
45//! // The solver interface is designed to allow adaptors to use multiple-threads / processes if
46//! // necessary. Therefore, the callback type requires all variables inside it to have a static
47//! // lifetime and to implement Send (i.e. the variable can be safely shared between theads).
48//! //
49//! // We use Arc<Mutex<T>> to create multiple references to a threadsafe mutable
50//! // variable of type T.
51//! //
52//! // Using the move |x| ... closure syntax, we move one of these references into the closure.
53//! // Note that a normal closure borrow variables from the parent so is not
54//! // thread-safe.
55//!
56//! let counter_ref = Arc::new(Mutex::new(0));
57//! let counter_ref_2 = counter_ref.clone();
58//! solver.solve(Box::new(move |_| {
59//!   let mut counter = (*counter_ref_2).lock().unwrap();
60//!   *counter += 1;
61//!   true
62//!   }));
63//!
64//! let mut counter = (*counter_ref).lock().unwrap();
65//! assert_eq!(*counter,2);
66//! ```
67//!
68//! # The Solver callback function
69//!
70//! The callback function given to `solve` is called whenever a solution is found by the solver.
71//!
72//! Its return value can be used to control how many solutions the solver finds:
73//!
74//! * If the callback function returns `true`, solver execution continues.
75//! * If the callback function returns `false`, the solver is terminated.
76//!
77
78// # Implementing Solver interfaces
79//
80// Solver interfaces can only be implemented inside this module, due to the SolverAdaptor crate
81// being sealed.
82//
83// To add support for a solver, implement the `SolverAdaptor` trait in a submodule.
84//
85// If incremental solving support is required, also implement a new `ModelModifier`. If this is not
86// required, all `ModelModifier` instances required by the SolverAdaptor trait can be replaced with
87// NotModifiable.
88//
89// For more details, see the docstrings for SolverAdaptor, ModelModifier, and NotModifiable.
90
91#![allow(dead_code)]
92#![allow(unused)]
93#![allow(clippy::manual_non_exhaustive)]
94
95use std::any::Any;
96use std::cell::OnceCell;
97use std::collections::HashMap;
98use std::error::Error;
99use std::fmt::{Debug, Display};
100use std::rc::Rc;
101use std::sync::{Arc, RwLock};
102use std::time::Instant;
103
104use schemars::JsonSchema;
105use serde::{Deserialize, Serialize};
106use strum_macros::{Display, EnumIter, EnumString};
107use thiserror::Error;
108
109use crate::ast::{Literal, Name};
110use crate::context::Context;
111use crate::stats::SolverStats;
112use crate::Model;
113
114use self::model_modifier::ModelModifier;
115use self::states::{ExecutionSuccess, Init, ModelLoaded, SolverState};
116
117pub mod adaptors;
118pub mod model_modifier;
119
120#[doc(hidden)]
121mod private;
122
123pub mod states;
124
125#[derive(
126    Debug,
127    EnumString,
128    EnumIter,
129    Display,
130    PartialEq,
131    Eq,
132    Hash,
133    Clone,
134    Copy,
135    Serialize,
136    Deserialize,
137    JsonSchema,
138)]
139pub enum SolverFamily {
140    SAT,
141    Minion,
142}
143
144/// The type for user-defined callbacks for use with [Solver].
145///
146/// Note that this enforces thread safety
147pub type SolverCallback = Box<dyn Fn(HashMap<Name, Literal>) -> bool + Send>;
148pub type SolverMutCallback =
149    Box<dyn Fn(HashMap<Name, Literal>, Box<dyn ModelModifier>) -> bool + Send>;
150
151/// A common interface for calling underlying solver APIs inside a [`Solver`].
152///
153/// Implementations of this trait aren't directly callable and should be used through [`Solver`] .
154///
155/// The below documentation lists the formal requirements that all implementations of
156/// [`SolverAdaptor`] should follow - **see the top level module documentation and [`Solver`] for
157/// usage details.**
158///
159/// # Encapsulation
160///
161///  The [`SolverAdaptor`] trait **must** only be implemented inside a submodule of this one,
162///  and **should** only be called through [`Solver`].
163///
164/// The `private::Sealed` trait and `private::Internal` type enforce these requirements by only
165/// allowing trait implementations and calling of methods of SolverAdaptor to occur inside this
166/// module.
167///
168/// # Thread Safety
169///
170/// Multiple instances of [`Solver`] can be run in parallel across multiple threads.
171///
172/// [`Solver`] provides no concurrency control or thread-safety; therefore, adaptors **must**
173/// ensure that multiple instances of themselves can be ran in parallel. This applies to all
174/// stages of solving including having two active `solve()` calls happening at a time, loading
175/// a model while another is mid-solve, loading two models at once, etc.
176///
177/// A [SolverAdaptor] **may** use whatever threading or process model it likes underneath the hood,
178/// as long as it obeys the above.
179///
180/// Method calls **should** block instead of erroring where possible.
181///
182/// Underlying solvers that only have one instance per process (such as Minion) **should** block
183/// (eg. using a [`Mutex<()>`](`std::sync::Mutex`)) to run calls to
184/// [`Solver<A,ModelLoaded>::solve()`] and [`Solver<A,ModelLoaded>::solve_mut()`] sequentially.
185pub trait SolverAdaptor: private::Sealed + Any {
186    /// Runs the solver on the given model.
187    ///
188    /// Implementations of this function **must** call the user provided callback whenever a solution
189    /// is found. If the user callback returns `true`, search should continue, if the user callback
190    /// returns `false`, search should terminate.
191    ///
192    /// # Returns
193    ///
194    /// If the solver terminates without crashing a [SolveSuccess] struct **must** returned. The
195    /// value of [SearchStatus] can be used to denote whether the underlying solver completed its
196    /// search or not. The latter case covers most non-crashing "failure" cases including user
197    /// termination, timeouts, etc.
198    ///
199    /// To help populate [SearchStatus], it may be helpful to implement counters that track if the
200    /// user callback has been called yet, and its return value. This information makes it is
201    /// possible to distinguish between the most common search statuses:
202    /// [SearchComplete::HasSolutions], [SearchComplete::NoSolutions], and
203    /// [SearchIncomplete::UserTerminated].
204    fn solve(
205        &mut self,
206        callback: SolverCallback,
207        _: private::Internal,
208    ) -> Result<SolveSuccess, SolverError>;
209
210    /// Runs the solver on the given model, allowing modification of the model through a
211    /// [`ModelModifier`].
212    ///
213    /// Implementations of this function **must** return [`OpNotSupported`](`ModificationFailure::OpNotSupported`)
214    /// if modifying the model mid-search is not supported.
215    ///
216    /// Otherwise, this should work in the same way as [`solve`](SolverAdaptor::solve).
217    fn solve_mut(
218        &mut self,
219        callback: SolverMutCallback,
220        _: private::Internal,
221    ) -> Result<SolveSuccess, SolverError>;
222    fn load_model(&mut self, model: Model, _: private::Internal) -> Result<(), SolverError>;
223    fn init_solver(&mut self, _: private::Internal) {}
224
225    /// Get the solver family that this solver adaptor belongs to
226    fn get_family(&self) -> SolverFamily;
227
228    /// Gets the name of the solver adaptor for pretty printing.
229    fn get_name(&self) -> Option<String> {
230        None
231    }
232
233    /// Adds the solver adaptor name and family (if they exist) to the given stats object.
234    fn add_adaptor_info_to_stats(&self, stats: SolverStats) -> SolverStats {
235        SolverStats {
236            solver_adaptor: self.get_name(),
237            solver_family: Some(self.get_family()),
238            ..stats
239        }
240    }
241}
242
243/// An abstract representation of a constraints solver.
244///
245/// [Solver] provides a common interface for interacting with a constraint solver. It also
246/// abstracts over solver-specific datatypes, handling the translation to/from [conjure_core::ast]
247/// types for a model and its solutions.
248///
249/// Details of how a model is solved is specified by the [SolverAdaptor]. This includes: the
250/// underlying solver used, the translation of the model to a solver compatible form, how solutions
251/// are translated back to [conjure_core::ast] types, and how incremental solving is implemented.
252/// As such, there may be multiple [SolverAdaptor] implementations for a single underlying solver:
253/// e.g. one adaptor may give solutions in a representation close to the solvers, while another may
254/// attempt to rewrite it back into Essence.
255///
256#[derive(Clone)]
257pub struct Solver<A: SolverAdaptor, State: SolverState = Init> {
258    state: State,
259    adaptor: A,
260    context: Option<Arc<RwLock<Context<'static>>>>,
261}
262
263impl<Adaptor: SolverAdaptor> Solver<Adaptor> {
264    pub fn new(solver_adaptor: Adaptor) -> Solver<Adaptor> {
265        let mut solver = Solver {
266            state: Init,
267            adaptor: solver_adaptor,
268            context: None,
269        };
270
271        solver.adaptor.init_solver(private::Internal);
272        solver
273    }
274
275    pub fn get_family(&self) -> SolverFamily {
276        self.adaptor.get_family()
277    }
278}
279
280impl<A: SolverAdaptor> Solver<A, Init> {
281    pub fn load_model(mut self, model: Model) -> Result<Solver<A, ModelLoaded>, SolverError> {
282        let solver_model = &mut self.adaptor.load_model(model.clone(), private::Internal)?;
283        Ok(Solver {
284            state: ModelLoaded,
285            adaptor: self.adaptor,
286            context: Some(model.context.clone()),
287        })
288    }
289}
290
291impl<A: SolverAdaptor> Solver<A, ModelLoaded> {
292    pub fn solve(
293        mut self,
294        callback: SolverCallback,
295    ) -> Result<Solver<A, ExecutionSuccess>, SolverError> {
296        #[allow(clippy::unwrap_used)]
297        let start_time = Instant::now();
298
299        #[allow(clippy::unwrap_used)]
300        let result = self.adaptor.solve(callback, private::Internal);
301
302        let duration = start_time.elapsed();
303
304        match result {
305            Ok(x) => {
306                let stats = self
307                    .adaptor
308                    .add_adaptor_info_to_stats(x.stats)
309                    .with_timings(duration.as_secs_f64());
310
311                Ok(Solver {
312                    adaptor: self.adaptor,
313                    state: ExecutionSuccess {
314                        stats,
315                        status: x.status,
316                        _sealed: private::Internal,
317                    },
318                    context: self.context,
319                })
320            }
321            Err(x) => Err(x),
322        }
323    }
324
325    pub fn solve_mut(
326        mut self,
327        callback: SolverMutCallback,
328    ) -> Result<Solver<A, ExecutionSuccess>, SolverError> {
329        #[allow(clippy::unwrap_used)]
330        let start_time = Instant::now();
331
332        #[allow(clippy::unwrap_used)]
333        let result = self.adaptor.solve_mut(callback, private::Internal);
334
335        let duration = start_time.elapsed();
336
337        match result {
338            Ok(x) => {
339                let stats = self
340                    .adaptor
341                    .add_adaptor_info_to_stats(x.stats)
342                    .with_timings(duration.as_secs_f64());
343
344                Ok(Solver {
345                    adaptor: self.adaptor,
346                    state: ExecutionSuccess {
347                        stats,
348                        status: x.status,
349                        _sealed: private::Internal,
350                    },
351                    context: self.context,
352                })
353            }
354            Err(x) => Err(x),
355        }
356    }
357}
358
359impl<A: SolverAdaptor> Solver<A, ExecutionSuccess> {
360    pub fn stats(&self) -> SolverStats {
361        self.state.stats.clone()
362    }
363
364    // Saves this solvers stats to the global context as a "solver run"
365    pub fn save_stats_to_context(&self) {
366        #[allow(clippy::unwrap_used)]
367        #[allow(clippy::expect_used)]
368        self.context
369            .as_ref()
370            .expect("")
371            .write()
372            .unwrap()
373            .stats
374            .add_solver_run(self.stats());
375    }
376
377    pub fn wall_time_s(&self) -> f64 {
378        self.stats().conjure_solver_wall_time_s
379    }
380}
381
382/// Errors returned by [Solver] on failure.
383#[non_exhaustive]
384#[derive(Debug, Error, Clone)]
385pub enum SolverError {
386    #[error("operation not implemented yet: {0}")]
387    OpNotImplemented(String),
388
389    #[error("operation not supported: {0}")]
390    OpNotSupported(String),
391
392    #[error("model feature not supported: {0}")]
393    ModelFeatureNotSupported(String),
394
395    #[error("model feature not implemented yet: {0}")]
396    ModelFeatureNotImplemented(String),
397
398    // use for semantics / type errors, use the above for syntax
399    #[error("model invalid: {0}")]
400    ModelInvalid(String),
401
402    #[error("error during solver execution: not implemented: {0}")]
403    RuntimeNotImplemented(String),
404
405    #[error("error during solver execution: {0}")]
406    Runtime(String),
407}
408
409/// Returned from [SolverAdaptor] when solving is successful.
410pub struct SolveSuccess {
411    stats: SolverStats,
412    status: SearchStatus,
413}
414
415pub enum SearchStatus {
416    /// The search was complete (i.e. the solver found all possible solutions)
417    Complete(SearchComplete),
418    /// The search was incomplete (i.e. it was terminated before all solutions were found)
419    Incomplete(SearchIncomplete),
420}
421
422#[non_exhaustive]
423pub enum SearchIncomplete {
424    Timeout,
425    UserTerminated,
426    #[doc(hidden)]
427    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
428    __NonExhaustive,
429}
430
431#[non_exhaustive]
432pub enum SearchComplete {
433    HasSolutions,
434    NoSolutions,
435    #[doc(hidden)]
436    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
437    __NonExhaustive,
438}