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}