Skip to main content

csp_solver/
config.rs

1//! Solver configuration vocabulary and the [`Csp<D>`] problem container.
2//!
3//! The pruning / propagation / optimization enums, the [`SolveConfig`] bundle
4//! (+ its `Default`), the [`SolveStats`] counters, and the `Csp<D>` struct
5//! definition. The builder surface lives in [`crate::csp`]; the search
6//! dispatch in [`crate::csp::solve`].
7//!
8//! Tests: `tests/solver.rs` (config matrix across every pruning × ordering).
9
10use crate::adjacency::Adjacency;
11use crate::cancel::CancelToken;
12use crate::constraint::ConstraintEnum;
13use crate::domain::Domain;
14use crate::ordering::Ordering;
15use crate::variable::Variable;
16
17/// Pruning strategy for backtracking search.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Pruning {
20    /// No pruning — pure backtracking.
21    None,
22    /// Forward checking: prune neighbors of the assigned variable.
23    ForwardChecking,
24    /// MAC: Maintaining Arc Consistency (AC-3 after each assignment).
25    Ac3,
26    /// Hybrid: forward checking + singleton propagation.
27    AcFc,
28}
29
30/// Propagation strategy for `propagate_with()`.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum PropagationStrategy {
33    /// Auto-select: AC-3 if finalize() was called, sweep otherwise.
34    Auto,
35    /// AC-3 worklist with adjacency graph. Requires finalize().
36    Ac3,
37    /// Fixed-point sweep over all constraints. No adjacency needed.
38    Sweep,
39}
40
41/// Optimization mode for the solver.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum OptimizationMode {
44    /// Find any feasible solution (existing behavior).
45    Feasibility,
46    /// Find the solution minimizing total cost (domain costs + soft penalties).
47    MinimizeCost,
48    /// Find the solution maximizing total cost.
49    MaximizeCost,
50}
51
52/// Solve configuration, isomorphic to Python's CSP constructor arguments.
53#[derive(Debug, Clone)]
54pub struct SolveConfig {
55    pub pruning: Pruning,
56    pub ordering: Ordering,
57    pub max_solutions: usize,
58    /// Enable Luby restarts with phase saving and (if `Ordering::Chs`) dynamic
59    /// conflict-history weighting. Opt-in; production sudoku stays `Ac3 + Mrv`.
60    /// The restart *driver* is not yet wired onto the unified kernel (see the
61    /// pass-3 composition report — chs backtrack.rs re-authoring is deferred);
62    /// today this flag is accepted but inert.
63    pub restarts: bool,
64    /// Optimization mode. Defaults to `Feasibility` (pure constraint satisfaction).
65    pub optimization_mode: OptimizationMode,
66    /// Maximum number of search nodes (backtrack / branch-and-bound
67    /// recursions) before the solver aborts early and returns whatever
68    /// solutions it has found so far. `None` disables the budget.
69    ///
70    /// Defaults to `Some(1_000_000)` so an unbounded pathological
71    /// search cannot hang a caller. When the budget is hit,
72    /// [`SolveStats::budget_exceeded`] is set to `true` on the
73    /// returning `Csp::stats()`. Callers that care about optimality
74    /// should branch on this flag and either accept the best-so-far
75    /// solution or fall back to a trivial per-variable pick.
76    pub node_budget: Option<u64>,
77    /// Cooperative cancellation flag, checked at the same cadence as
78    /// `node_budget`. `None` (the default) means the search cannot be
79    /// cancelled externally. Set this to a [`CancelToken`] clone and keep
80    /// another clone on the calling side to request an early stop — e.g.
81    /// from Python, released via `Python::allow_threads`, when an
82    /// `asyncio.wait_for` timeout elapses. See [`SolveStats::cancelled`].
83    pub cancel: Option<CancelToken>,
84}
85
86impl Default for SolveConfig {
87    /// Ac3 + FailFirst (Pass-2 D6, ratified; ships in 0.2.0): the production
88    /// posture every measured surface converged on. Coordination-gated with
89    /// bbnf-lang's two live `finalize()+solve_optimized()` consumers via the
90    /// enforced-compile sync gate (`sync-csp-solver-vendor.sh --verify`).
91    fn default() -> Self {
92        Self {
93            pruning: Pruning::Ac3,
94            ordering: Ordering::FailFirst,
95            max_solutions: 1,
96            restarts: false,
97            optimization_mode: OptimizationMode::Feasibility,
98            node_budget: Some(1_000_000),
99            cancel: None,
100        }
101    }
102}
103
104/// Solver statistics.
105#[derive(Debug, Clone, Default)]
106pub struct SolveStats {
107    pub backtracks: u64,
108    pub nodes_explored: u64,
109    pub propagations: u64,
110    /// Set to `true` when the last search hit its
111    /// [`SolveConfig::node_budget`] and aborted early.
112    /// Solutions returned alongside this flag are best-so-far, not
113    /// necessarily optimal.
114    pub budget_exceeded: bool,
115    /// Set to `true` when the last search was stopped early via
116    /// [`SolveConfig::cancel`] rather than hitting `node_budget`.
117    /// Distinct from `budget_exceeded`: this is an externally requested
118    /// stop, not a self-imposed cap.
119    pub cancelled: bool,
120}
121
122/// The main CSP solver struct.
123///
124/// Generic over the domain type `D`. Build a problem by adding variables and
125/// constraints, call `finalize()` to build the adjacency graph, then `solve()`.
126///
127/// The builder methods (`add_*`, `finalize`) live in [`crate::csp`]; the
128/// solve/propagate dispatch in [`crate::csp::solve`].
129pub struct Csp<D: Domain> {
130    pub variables: Vec<Variable<D>>,
131    pub(crate) constraints: Vec<ConstraintEnum<D>>,
132    pub(crate) adjacency: Option<Adjacency>,
133    pub(crate) stats: SolveStats,
134    /// Per-constraint weights for the Mrv / Chs weighted-degree scan.
135    pub(crate) constraint_weights: Vec<f64>,
136    /// For each variable, the indices of constraints involving it.
137    pub(crate) var_constraint_ids: Vec<Vec<usize>>,
138}