1pub mod adjacency;
12pub mod builder;
13pub mod constraint;
14pub mod domain;
15pub mod ordering;
16#[cfg(feature = "py")]
17pub mod py;
18pub mod puzzles;
19pub mod solver;
20pub mod variable;
21
22pub use builder::assignment::{
23 AssignmentBuilder, AssignmentError, AssignmentSolution, SENTINEL, assignment,
24};
25pub use puzzles::sudoku;
26
27use adjacency::Adjacency;
28use constraint::{AllDifferent, Constraint, ConstraintEnum, NotEqual, SoftLambdaConstraint, VarId};
29use domain::Domain;
30use ordering::Ordering;
31use solver::backjump::{self, BackjumpConfig};
32use solver::backtrack::{self, BacktrackConfig, Solution};
33use solver::optimize;
34use variable::Variable;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Pruning {
39 None,
41 ForwardChecking,
43 Ac3,
45 AcFc,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum PropagationStrategy {
52 Auto,
54 Ac3,
56 Sweep,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum OptimizationMode {
63 Feasibility,
65 MinimizeCost,
67 MaximizeCost,
69}
70
71#[derive(Debug, Clone)]
73pub struct SolveConfig {
74 pub pruning: Pruning,
75 pub ordering: Ordering,
76 pub max_solutions: usize,
77 pub backjumping: bool,
79 pub optimization_mode: OptimizationMode,
81 pub node_budget: Option<u64>,
92}
93
94impl Default for SolveConfig {
95 fn default() -> Self {
96 Self {
97 pruning: Pruning::ForwardChecking,
98 ordering: Ordering::Chronological,
99 max_solutions: 1,
100 backjumping: false,
101 optimization_mode: OptimizationMode::Feasibility,
102 node_budget: Some(1_000_000),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct SolveStats {
110 pub backtracks: u64,
111 pub nodes_explored: u64,
112 pub propagations: u64,
113 pub budget_exceeded: bool,
118}
119
120pub struct Csp<D: Domain> {
125 pub variables: Vec<Variable<D>>,
126 constraints: Vec<ConstraintEnum<D>>,
127 adjacency: Option<Adjacency>,
128 stats: SolveStats,
129 constraint_weights: Vec<f64>,
131 var_constraint_ids: Vec<Vec<usize>>,
133}
134
135impl<D: Domain> Csp<D> {
136 pub fn new() -> Self {
138 Self {
139 variables: Vec::new(),
140 constraints: Vec::new(),
141 adjacency: None,
142 stats: SolveStats::default(),
143 constraint_weights: Vec::new(),
144 var_constraint_ids: Vec::new(),
145 }
146 }
147
148 pub fn add_variable(&mut self, domain: D) -> VarId {
150 let id = self.variables.len() as VarId;
151 self.variables.push(Variable::new(domain));
152 id
153 }
154
155 pub fn add_variables(&mut self, domain: &D, count: usize) -> Vec<VarId> {
157 (0..count)
158 .map(|_| self.add_variable(domain.clone()))
159 .collect()
160 }
161
162 pub fn add_constraint(&mut self, c: impl Constraint<D> + 'static) {
164 self.constraints.push(ConstraintEnum::Custom(Box::new(c)));
165 }
166
167 pub fn add_constraint_enum(&mut self, c: ConstraintEnum<D>) {
169 self.constraints.push(c);
170 }
171
172 pub fn add_soft_constraint(&mut self, c: SoftLambdaConstraint<D>) {
174 self.constraints.push(ConstraintEnum::Soft(c));
175 }
176
177 pub fn add_not_equal(&mut self, x: VarId, y: VarId) {
179 self.constraints.push(ConstraintEnum::NotEqual(NotEqual::new(x, y)));
180 }
181
182 pub fn add_all_different(&mut self, vars: Vec<VarId>) {
184 self.constraints.push(ConstraintEnum::AllDifferent(AllDifferent::new(vars)));
185 }
186
187 pub fn add_equals(&mut self, var: VarId, value: D::Value)
189 where
190 D: 'static,
191 {
192 self.add_constraint(constraint::LambdaConstraint::new(
193 vec![var],
194 move |assignment| match &assignment[var as usize] {
195 Some(v) => *v == value,
196 None => true,
197 },
198 format!("equals({var})"),
199 ));
200 }
201
202 pub fn add_less_than(&mut self, x: VarId, y: VarId)
204 where
205 D: 'static, D::Value: PartialOrd,
206 {
207 self.add_constraint(constraint::LambdaConstraint::new(
208 vec![x, y],
209 move |assignment| match (&assignment[x as usize], &assignment[y as usize]) {
210 (Some(a), Some(b)) => a < b,
211 _ => true,
212 },
213 format!("less_than({x},{y})"),
214 ));
215 }
216
217 pub fn add_greater_than(&mut self, x: VarId, y: VarId)
219 where
220 D: 'static, D::Value: PartialOrd,
221 {
222 self.add_constraint(constraint::LambdaConstraint::new(
223 vec![x, y],
224 move |assignment| match (&assignment[x as usize], &assignment[y as usize]) {
225 (Some(a), Some(b)) => a > b,
226 _ => true,
227 },
228 format!("greater_than({x},{y})"),
229 ));
230 }
231
232 pub fn finalize(&mut self)
235 where
236 D::Value: PartialEq,
237 {
238 let num_vars = self.variables.len();
239 self.adjacency = Some(Adjacency::build(num_vars, &self.constraints));
240
241 self.constraint_weights = vec![1.0; self.constraints.len()];
242 self.var_constraint_ids = vec![Vec::new(); num_vars];
243 for (ci, c) in self.constraints.iter().enumerate() {
244 for &v in c.scope() {
245 self.var_constraint_ids[v as usize].push(ci);
246 }
247 }
248 }
249
250 pub fn propagate(&mut self) -> Result<(), Unsatisfiable>
252 where
253 D::Value: PartialEq,
254 {
255 self.propagate_with(PropagationStrategy::Auto)
256 }
257
258 pub fn propagate_with(&mut self, strategy: PropagationStrategy) -> Result<(), Unsatisfiable>
260 where
261 D::Value: PartialEq,
262 {
263 match strategy {
264 PropagationStrategy::Auto => {
265 if self.adjacency.is_some() {
266 self.propagate_with(PropagationStrategy::Ac3)
267 } else {
268 self.propagate_with(PropagationStrategy::Sweep)
269 }
270 }
271 PropagationStrategy::Ac3 => {
272 let adjacency = self.adjacency.as_ref().ok_or(Unsatisfiable)?.clone();
273 solver::ac3::ac3_full(
274 &mut self.variables,
275 &self.constraints,
276 &adjacency,
277 &mut self.stats,
278 0,
279 )
280 }
281 PropagationStrategy::Sweep => {
282 solver::monotonic::propagate_monotonic(
283 &mut self.variables,
284 &self.constraints,
285 &mut self.stats,
286 )
287 }
288 }
289 }
290
291 pub fn solve(&mut self, config: &SolveConfig) -> Vec<Solution<D>>
297 where
298 D::Value: PartialEq,
299 {
300 let adjacency = self
301 .adjacency
302 .as_ref()
303 .expect("call finalize() before solve()")
304 .clone();
305
306 self.stats = SolveStats::default();
307
308 for v in &mut self.variables {
310 v.reset();
311 }
312
313 match config.optimization_mode {
314 OptimizationMode::Feasibility => {
315 if config.backjumping {
316 let bj_config = BackjumpConfig {
317 pruning: config.pruning,
318 ordering: config.ordering,
319 max_solutions: config.max_solutions,
320 constraint_weights: self.constraint_weights.clone(),
321 var_constraint_ids: self.var_constraint_ids.clone(),
322 node_budget: config.node_budget,
323 };
324 backjump::backjump_search(
325 &mut self.variables,
326 &self.constraints,
327 &adjacency,
328 &bj_config,
329 &mut self.stats,
330 )
331 } else {
332 let bt_config = BacktrackConfig {
333 pruning: config.pruning,
334 ordering: config.ordering,
335 max_solutions: config.max_solutions,
336 constraint_weights: self.constraint_weights.clone(),
337 var_constraint_ids: self.var_constraint_ids.clone(),
338 node_budget: config.node_budget,
339 };
340 backtrack::backtrack_search(
341 &mut self.variables,
342 &self.constraints,
343 &adjacency,
344 &bt_config,
345 &mut self.stats,
346 )
347 }
348 }
349 mode @ (OptimizationMode::MinimizeCost | OptimizationMode::MaximizeCost) => {
350 let opt_config = optimize::OptimizeConfig {
351 pruning: config.pruning,
352 ordering: config.ordering,
353 max_solutions: config.max_solutions,
354 constraint_weights: self.constraint_weights.clone(),
355 var_constraint_ids: self.var_constraint_ids.clone(),
356 maximize: mode == OptimizationMode::MaximizeCost,
357 node_budget: config.node_budget,
358 };
359 optimize::branch_and_bound(
362 &mut self.variables,
363 &self.constraints,
364 &adjacency,
365 &opt_config,
366 &mut self.stats,
367 &optimize::ZeroCost,
368 )
369 }
370 }
371 }
372
373 pub fn solve_with_given(
378 &mut self,
379 config: &SolveConfig,
380 given: &[(VarId, D::Value)],
381 ) -> Vec<Solution<D>>
382 where
383 D::Value: PartialEq,
384 {
385 let adjacency = self
386 .adjacency
387 .as_ref()
388 .expect("call finalize() before solve_with_given()")
389 .clone();
390
391 self.stats = SolveStats::default();
392
393 for v in &mut self.variables {
395 v.reset();
396 }
397
398 for (var, val) in given {
400 let v = &mut self.variables[*var as usize];
401 let vals: Vec<_> = v.domain.iter().collect();
402 for dv in &vals {
403 if dv != val {
404 v.domain.remove(dv);
405 }
406 }
407 }
408
409 for (var, val) in given {
411 for &neighbor in adjacency.neighbors_of_var(*var) {
412 let is_given = given.iter().any(|(gv, _)| *gv == neighbor);
413 if !is_given {
414 self.variables[neighbor as usize].domain.remove(val);
415 }
416 }
417 }
418
419 let _ = solver::ac3::ac3_full(
421 &mut self.variables,
422 &self.constraints,
423 &adjacency,
424 &mut self.stats,
425 0, );
427
428 let bt_config = BacktrackConfig {
429 pruning: config.pruning,
430 ordering: config.ordering,
431 max_solutions: config.max_solutions,
432 constraint_weights: self.constraint_weights.clone(),
433 var_constraint_ids: self.var_constraint_ids.clone(),
434 node_budget: config.node_budget,
435 };
436
437 backtrack::backtrack_search_with_given(
438 &mut self.variables,
439 &self.constraints,
440 &adjacency,
441 &bt_config,
442 &mut self.stats,
443 given,
444 )
445 }
446
447 pub fn solve_with_cost_eval(
455 &mut self,
456 config: &SolveConfig,
457 cost_eval: &dyn optimize::DomainCostEval<D>,
458 ) -> Vec<Solution<D>>
459 where
460 D::Value: PartialEq,
461 {
462 let adjacency = self
463 .adjacency
464 .as_ref()
465 .expect("call finalize() before solve_with_cost_eval()")
466 .clone();
467
468 self.stats = SolveStats::default();
469 for v in &mut self.variables {
470 v.reset();
471 }
472
473 let mode = config.optimization_mode;
474 let opt_config = optimize::OptimizeConfig {
475 pruning: config.pruning,
476 ordering: config.ordering,
477 max_solutions: config.max_solutions,
478 constraint_weights: self.constraint_weights.clone(),
479 var_constraint_ids: self.var_constraint_ids.clone(),
480 maximize: mode == OptimizationMode::MaximizeCost,
481 node_budget: config.node_budget,
482 };
483 optimize::branch_and_bound(
484 &mut self.variables,
485 &self.constraints,
486 &adjacency,
487 &opt_config,
488 &mut self.stats,
489 cost_eval,
490 )
491 }
492
493 pub fn stats(&self) -> &SolveStats {
495 &self.stats
496 }
497
498 pub fn adjacency(&self) -> Option<&Adjacency> {
500 self.adjacency.as_ref()
501 }
502}
503
504impl<D: Domain> Default for Csp<D> {
505 fn default() -> Self {
506 Self::new()
507 }
508}
509
510impl<D: domain::CostDomain> Csp<D> {
511 pub fn solve_optimized(&mut self, config: &SolveConfig) -> Vec<Solution<D>>
515 where
516 D::Value: PartialEq,
517 {
518 self.solve_with_cost_eval(config, &optimize::CostDomainEval)
519 }
520}
521
522#[derive(Debug, Clone)]
524pub struct Unsatisfiable;
525
526impl std::fmt::Display for Unsatisfiable {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 write!(f, "CSP is unsatisfiable")
529 }
530}
531
532impl std::error::Error for Unsatisfiable {}