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