pub mod adjacency;
pub mod builder;
pub mod constraint;
pub mod domain;
pub mod ordering;
#[cfg(feature = "py")]
pub mod py;
pub mod puzzles;
pub mod solver;
pub mod variable;
pub use builder::assignment::{
AssignmentBuilder, AssignmentError, AssignmentSolution, SENTINEL, assignment,
};
pub use puzzles::sudoku;
use adjacency::Adjacency;
use constraint::{AllDifferent, Constraint, ConstraintEnum, NotEqual, SoftLambdaConstraint, VarId};
use domain::Domain;
use ordering::Ordering;
use solver::backjump::{self, BackjumpConfig};
use solver::backtrack::{self, BacktrackConfig, Solution};
use solver::optimize;
use variable::Variable;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pruning {
None,
ForwardChecking,
Ac3,
AcFc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropagationStrategy {
Auto,
Ac3,
Sweep,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptimizationMode {
Feasibility,
MinimizeCost,
MaximizeCost,
}
#[derive(Debug, Clone)]
pub struct SolveConfig {
pub pruning: Pruning,
pub ordering: Ordering,
pub max_solutions: usize,
pub backjumping: bool,
pub optimization_mode: OptimizationMode,
pub node_budget: Option<u64>,
}
impl Default for SolveConfig {
fn default() -> Self {
Self {
pruning: Pruning::ForwardChecking,
ordering: Ordering::Chronological,
max_solutions: 1,
backjumping: false,
optimization_mode: OptimizationMode::Feasibility,
node_budget: Some(1_000_000),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SolveStats {
pub backtracks: u64,
pub nodes_explored: u64,
pub propagations: u64,
pub budget_exceeded: bool,
}
pub struct Csp<D: Domain> {
pub variables: Vec<Variable<D>>,
constraints: Vec<ConstraintEnum<D>>,
adjacency: Option<Adjacency>,
stats: SolveStats,
constraint_weights: Vec<f64>,
var_constraint_ids: Vec<Vec<usize>>,
}
impl<D: Domain> Csp<D> {
pub fn new() -> Self {
Self {
variables: Vec::new(),
constraints: Vec::new(),
adjacency: None,
stats: SolveStats::default(),
constraint_weights: Vec::new(),
var_constraint_ids: Vec::new(),
}
}
pub fn add_variable(&mut self, domain: D) -> VarId {
let id = self.variables.len() as VarId;
self.variables.push(Variable::new(domain));
id
}
pub fn add_variables(&mut self, domain: &D, count: usize) -> Vec<VarId> {
(0..count)
.map(|_| self.add_variable(domain.clone()))
.collect()
}
pub fn add_constraint(&mut self, c: impl Constraint<D> + 'static) {
self.constraints.push(ConstraintEnum::Custom(Box::new(c)));
}
pub fn add_constraint_enum(&mut self, c: ConstraintEnum<D>) {
self.constraints.push(c);
}
pub fn add_soft_constraint(&mut self, c: SoftLambdaConstraint<D>) {
self.constraints.push(ConstraintEnum::Soft(c));
}
pub fn add_not_equal(&mut self, x: VarId, y: VarId) {
self.constraints.push(ConstraintEnum::NotEqual(NotEqual::new(x, y)));
}
pub fn add_all_different(&mut self, vars: Vec<VarId>) {
self.constraints.push(ConstraintEnum::AllDifferent(AllDifferent::new(vars)));
}
pub fn add_equals(&mut self, var: VarId, value: D::Value)
where
D: 'static,
{
self.add_constraint(constraint::LambdaConstraint::new(
vec![var],
move |assignment| match &assignment[var as usize] {
Some(v) => *v == value,
None => true,
},
format!("equals({var})"),
));
}
pub fn add_less_than(&mut self, x: VarId, y: VarId)
where
D: 'static, D::Value: PartialOrd,
{
self.add_constraint(constraint::LambdaConstraint::new(
vec![x, y],
move |assignment| match (&assignment[x as usize], &assignment[y as usize]) {
(Some(a), Some(b)) => a < b,
_ => true,
},
format!("less_than({x},{y})"),
));
}
pub fn add_greater_than(&mut self, x: VarId, y: VarId)
where
D: 'static, D::Value: PartialOrd,
{
self.add_constraint(constraint::LambdaConstraint::new(
vec![x, y],
move |assignment| match (&assignment[x as usize], &assignment[y as usize]) {
(Some(a), Some(b)) => a > b,
_ => true,
},
format!("greater_than({x},{y})"),
));
}
pub fn finalize(&mut self)
where
D::Value: PartialEq,
{
let num_vars = self.variables.len();
self.adjacency = Some(Adjacency::build(num_vars, &self.constraints));
self.constraint_weights = vec![1.0; self.constraints.len()];
self.var_constraint_ids = vec![Vec::new(); num_vars];
for (ci, c) in self.constraints.iter().enumerate() {
for &v in c.scope() {
self.var_constraint_ids[v as usize].push(ci);
}
}
}
pub fn propagate(&mut self) -> Result<(), Unsatisfiable>
where
D::Value: PartialEq,
{
self.propagate_with(PropagationStrategy::Auto)
}
pub fn propagate_with(&mut self, strategy: PropagationStrategy) -> Result<(), Unsatisfiable>
where
D::Value: PartialEq,
{
match strategy {
PropagationStrategy::Auto => {
if self.adjacency.is_some() {
self.propagate_with(PropagationStrategy::Ac3)
} else {
self.propagate_with(PropagationStrategy::Sweep)
}
}
PropagationStrategy::Ac3 => {
let adjacency = self.adjacency.as_ref().ok_or(Unsatisfiable)?.clone();
solver::ac3::ac3_full(
&mut self.variables,
&self.constraints,
&adjacency,
&mut self.stats,
0,
)
}
PropagationStrategy::Sweep => {
solver::monotonic::propagate_monotonic(
&mut self.variables,
&self.constraints,
&mut self.stats,
)
}
}
}
pub fn solve(&mut self, config: &SolveConfig) -> Vec<Solution<D>>
where
D::Value: PartialEq,
{
let adjacency = self
.adjacency
.as_ref()
.expect("call finalize() before solve()")
.clone();
self.stats = SolveStats::default();
for v in &mut self.variables {
v.reset();
}
match config.optimization_mode {
OptimizationMode::Feasibility => {
if config.backjumping {
let bj_config = BackjumpConfig {
pruning: config.pruning,
ordering: config.ordering,
max_solutions: config.max_solutions,
constraint_weights: self.constraint_weights.clone(),
var_constraint_ids: self.var_constraint_ids.clone(),
node_budget: config.node_budget,
};
backjump::backjump_search(
&mut self.variables,
&self.constraints,
&adjacency,
&bj_config,
&mut self.stats,
)
} else {
let bt_config = BacktrackConfig {
pruning: config.pruning,
ordering: config.ordering,
max_solutions: config.max_solutions,
constraint_weights: self.constraint_weights.clone(),
var_constraint_ids: self.var_constraint_ids.clone(),
node_budget: config.node_budget,
};
backtrack::backtrack_search(
&mut self.variables,
&self.constraints,
&adjacency,
&bt_config,
&mut self.stats,
)
}
}
mode @ (OptimizationMode::MinimizeCost | OptimizationMode::MaximizeCost) => {
let opt_config = optimize::OptimizeConfig {
pruning: config.pruning,
ordering: config.ordering,
max_solutions: config.max_solutions,
constraint_weights: self.constraint_weights.clone(),
var_constraint_ids: self.var_constraint_ids.clone(),
maximize: mode == OptimizationMode::MaximizeCost,
node_budget: config.node_budget,
};
optimize::branch_and_bound(
&mut self.variables,
&self.constraints,
&adjacency,
&opt_config,
&mut self.stats,
&optimize::ZeroCost,
)
}
}
}
pub fn solve_with_given(
&mut self,
config: &SolveConfig,
given: &[(VarId, D::Value)],
) -> Vec<Solution<D>>
where
D::Value: PartialEq,
{
let adjacency = self
.adjacency
.as_ref()
.expect("call finalize() before solve_with_given()")
.clone();
self.stats = SolveStats::default();
for v in &mut self.variables {
v.reset();
}
for (var, val) in given {
let v = &mut self.variables[*var as usize];
let vals: Vec<_> = v.domain.iter().collect();
for dv in &vals {
if dv != val {
v.domain.remove(dv);
}
}
}
for (var, val) in given {
for &neighbor in adjacency.neighbors_of_var(*var) {
let is_given = given.iter().any(|(gv, _)| *gv == neighbor);
if !is_given {
self.variables[neighbor as usize].domain.remove(val);
}
}
}
let _ = solver::ac3::ac3_full(
&mut self.variables,
&self.constraints,
&adjacency,
&mut self.stats,
0, );
let bt_config = BacktrackConfig {
pruning: config.pruning,
ordering: config.ordering,
max_solutions: config.max_solutions,
constraint_weights: self.constraint_weights.clone(),
var_constraint_ids: self.var_constraint_ids.clone(),
node_budget: config.node_budget,
};
backtrack::backtrack_search_with_given(
&mut self.variables,
&self.constraints,
&adjacency,
&bt_config,
&mut self.stats,
given,
)
}
pub fn solve_with_cost_eval(
&mut self,
config: &SolveConfig,
cost_eval: &dyn optimize::DomainCostEval<D>,
) -> Vec<Solution<D>>
where
D::Value: PartialEq,
{
let adjacency = self
.adjacency
.as_ref()
.expect("call finalize() before solve_with_cost_eval()")
.clone();
self.stats = SolveStats::default();
for v in &mut self.variables {
v.reset();
}
let mode = config.optimization_mode;
let opt_config = optimize::OptimizeConfig {
pruning: config.pruning,
ordering: config.ordering,
max_solutions: config.max_solutions,
constraint_weights: self.constraint_weights.clone(),
var_constraint_ids: self.var_constraint_ids.clone(),
maximize: mode == OptimizationMode::MaximizeCost,
node_budget: config.node_budget,
};
optimize::branch_and_bound(
&mut self.variables,
&self.constraints,
&adjacency,
&opt_config,
&mut self.stats,
cost_eval,
)
}
pub fn stats(&self) -> &SolveStats {
&self.stats
}
pub fn adjacency(&self) -> Option<&Adjacency> {
self.adjacency.as_ref()
}
}
impl<D: Domain> Default for Csp<D> {
fn default() -> Self {
Self::new()
}
}
impl<D: domain::CostDomain> Csp<D> {
pub fn solve_optimized(&mut self, config: &SolveConfig) -> Vec<Solution<D>>
where
D::Value: PartialEq,
{
self.solve_with_cost_eval(config, &optimize::CostDomainEval)
}
}
#[derive(Debug, Clone)]
pub struct Unsatisfiable;
impl std::fmt::Display for Unsatisfiable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CSP is unsatisfiable")
}
}
impl std::error::Error for Unsatisfiable {}