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