Skip to main content

ipfrs_tensorlogic/
constraint_solver.rs

1//! Constraint Satisfaction Problem (CSP) solver with backtracking search,
2//! arc consistency (AC-3), and heuristics for variable/value ordering.
3//!
4//! # Example
5//!
6//! ```
7//! use ipfrs_tensorlogic::constraint_solver::{
8//!     ConstraintSolver, Constraint, CspVarId, SolverConfig,
9//! };
10//!
11//! let config = SolverConfig::default();
12//! let mut solver = ConstraintSolver::new(config);
13//!
14//! let x = solver.add_variable("x".to_string(), vec![1, 2, 3]);
15//! let y = solver.add_variable("y".to_string(), vec![1, 2, 3]);
16//! solver.add_constraint(Constraint::NotEqual(x, y));
17//!
18//! let result = solver.solve();
19//! assert!(!result.solutions.is_empty());
20//! let sol = &result.solutions[0];
21//! assert_ne!(sol.get(x), sol.get(y));
22//! ```
23
24use std::collections::{HashMap, VecDeque};
25use std::time::Instant;
26
27// ---------------------------------------------------------------------------
28// CspVarId
29// ---------------------------------------------------------------------------
30
31/// Opaque index for a CSP variable.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct CspVarId(pub usize);
34
35// ---------------------------------------------------------------------------
36// Domain
37// ---------------------------------------------------------------------------
38
39/// The current set of allowed integer values for a CSP variable.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Domain {
42    /// Ordered, deduplicated allowed values. Maintained sorted.
43    pub values: Vec<i64>,
44}
45
46impl Domain {
47    /// Create a domain from a `Vec<i64>`. Values are sorted and deduplicated.
48    pub fn new(mut values: Vec<i64>) -> Self {
49        values.sort_unstable();
50        values.dedup();
51        Self { values }
52    }
53
54    /// Return `true` when the domain has no values remaining.
55    #[inline]
56    pub fn is_empty(&self) -> bool {
57        self.values.is_empty()
58    }
59
60    /// Return `true` when `v` is a member of the domain.
61    pub fn contains(&self, v: i64) -> bool {
62        self.values.binary_search(&v).is_ok()
63    }
64
65    /// Remove `v` from the domain. Returns `true` when `v` was present.
66    pub fn remove(&mut self, v: i64) -> bool {
67        if let Ok(idx) = self.values.binary_search(&v) {
68            self.values.remove(idx);
69            true
70        } else {
71            false
72        }
73    }
74
75    /// Number of values in the domain.
76    #[inline]
77    pub fn len(&self) -> usize {
78        self.values.len()
79    }
80}
81
82// ---------------------------------------------------------------------------
83// Constraint
84// ---------------------------------------------------------------------------
85
86/// A constraint relating one or more CSP variables.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum Constraint {
89    /// All listed variables must take pairwise-distinct values.
90    AllDifferent(Vec<CspVarId>),
91    /// Two variables must be equal: `a == b`.
92    Equal(CspVarId, CspVarId),
93    /// Two variables must differ: `a != b`.
94    NotEqual(CspVarId, CspVarId),
95    /// Strict ordering: `a < b`.
96    LessThan(CspVarId, CspVarId),
97    /// Non-strict ordering: `a <= b`.
98    LessEqual(CspVarId, CspVarId),
99    /// Sum of values must equal `target`: `Σ vars[i] == target`.
100    Sum {
101        /// Variables to sum.
102        vars: Vec<CspVarId>,
103        /// Required total.
104        target: i64,
105    },
106    /// Variable must take a value from the `allowed` list.
107    InDomain {
108        /// The variable to restrict.
109        var: CspVarId,
110        /// Allowed values (does not need to be sorted).
111        allowed: Vec<i64>,
112    },
113}
114
115impl Constraint {
116    /// Return the set of variable ids that participate in this constraint.
117    pub fn variables(&self) -> Vec<CspVarId> {
118        match self {
119            Constraint::AllDifferent(vars) => vars.clone(),
120            Constraint::Equal(a, b)
121            | Constraint::NotEqual(a, b)
122            | Constraint::LessThan(a, b)
123            | Constraint::LessEqual(a, b) => vec![*a, *b],
124            Constraint::Sum { vars, .. } => vars.clone(),
125            Constraint::InDomain { var, .. } => vec![*var],
126        }
127    }
128
129    /// Return `true` when `var` is mentioned in this constraint.
130    pub fn involves(&self, var: CspVarId) -> bool {
131        self.variables().contains(&var)
132    }
133
134    /// Return `true` when `xi` and `xj` are both mentioned (binary relationship).
135    fn involves_pair(&self, xi: CspVarId, xj: CspVarId) -> bool {
136        let vars = self.variables();
137        vars.contains(&xi) && vars.contains(&xj)
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Assignment
143// ---------------------------------------------------------------------------
144
145/// A (partial or complete) mapping from variable ids to assigned values.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Assignment {
148    /// Internal map: variable index → assigned value.
149    pub values: HashMap<usize, i64>,
150}
151
152impl Assignment {
153    /// Create an empty assignment.
154    pub fn new() -> Self {
155        Self {
156            values: HashMap::new(),
157        }
158    }
159
160    /// Return `true` when all `num_vars` variables have been assigned.
161    #[inline]
162    pub fn is_complete(&self, num_vars: usize) -> bool {
163        self.values.len() == num_vars
164    }
165
166    /// Retrieve the assigned value for `var`, if any.
167    #[inline]
168    pub fn get(&self, var: CspVarId) -> Option<i64> {
169        self.values.get(&var.0).copied()
170    }
171
172    /// Assign `value` to `var`.
173    #[inline]
174    pub fn set(&mut self, var: CspVarId, value: i64) {
175        self.values.insert(var.0, value);
176    }
177
178    /// Remove the assignment for `var`.
179    #[inline]
180    pub fn unset(&mut self, var: CspVarId) {
181        self.values.remove(&var.0);
182    }
183
184    /// Iterate over all (var_id, value) pairs.
185    pub fn iter(&self) -> impl Iterator<Item = (CspVarId, i64)> + '_ {
186        self.values.iter().map(|(&id, &v)| (CspVarId(id), v))
187    }
188}
189
190impl Default for Assignment {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196// ---------------------------------------------------------------------------
197// CspVariable
198// ---------------------------------------------------------------------------
199
200/// A CSP variable: its identity, name, and initial domain.
201#[derive(Debug, Clone)]
202pub struct CspVariable {
203    /// Unique identifier (index in the solver's variable list).
204    pub id: CspVarId,
205    /// Human-readable name.
206    pub name: String,
207    /// Initial domain (before any pruning).
208    pub domain: Domain,
209}
210
211// ---------------------------------------------------------------------------
212// SolverConfig
213// ---------------------------------------------------------------------------
214
215/// Configuration knobs for [`ConstraintSolver`].
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct SolverConfig {
218    /// Maximum number of solutions to collect before stopping.
219    pub max_solutions: usize,
220    /// When `true`, run AC-3 arc consistency before backtracking.
221    pub use_ac3: bool,
222    /// When `true`, apply the Minimum Remaining Values (MRV) heuristic when
223    /// choosing the next variable to assign.
224    pub use_mrv: bool,
225    /// When `true`, apply the Least Constraining Value (LCV) heuristic to
226    /// order domain values. Currently sorts ascending as a lightweight proxy.
227    pub use_lcv: bool,
228    /// Maximum number of backtrack steps before aborting (0 = unlimited).
229    pub max_backtracks: usize,
230}
231
232impl Default for SolverConfig {
233    fn default() -> Self {
234        Self {
235            max_solutions: 1,
236            use_ac3: true,
237            use_mrv: true,
238            use_lcv: false,
239            max_backtracks: 100_000,
240        }
241    }
242}
243
244// ---------------------------------------------------------------------------
245// SolverResult
246// ---------------------------------------------------------------------------
247
248/// Statistics and solutions returned by [`ConstraintSolver::solve`].
249#[derive(Debug, Clone)]
250pub struct SolverResult {
251    /// All complete assignments found (up to `max_solutions`).
252    pub solutions: Vec<Assignment>,
253    /// Total number of backtrack steps taken during search.
254    pub backtracks: u64,
255    /// Total number of constraint checks performed.
256    pub constraint_checks: u64,
257    /// Wall-clock time spent in `solve` (milliseconds).
258    pub time_ms: u64,
259}
260
261impl SolverResult {
262    fn new() -> Self {
263        Self {
264            solutions: Vec::new(),
265            backtracks: 0,
266            constraint_checks: 0,
267            time_ms: 0,
268        }
269    }
270}
271
272// ---------------------------------------------------------------------------
273// CspError
274// ---------------------------------------------------------------------------
275
276/// Error conditions that can arise during CSP solving.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum CspError {
279    /// A constraint referenced a variable id that does not exist.
280    VariableNotFound(usize),
281    /// A constraint is structurally invalid (e.g., empty variable list).
282    InvalidConstraint(String),
283    /// AC-3 proved the problem is unsatisfiable before backtracking began.
284    UnsatisfiableAfterAC3,
285}
286
287impl std::fmt::Display for CspError {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        match self {
290            CspError::VariableNotFound(id) => write!(f, "Variable not found: {}", id),
291            CspError::InvalidConstraint(msg) => write!(f, "Invalid constraint: {}", msg),
292            CspError::UnsatisfiableAfterAC3 => {
293                write!(f, "Problem is unsatisfiable after AC-3 preprocessing")
294            }
295        }
296    }
297}
298
299impl std::error::Error for CspError {}
300
301// ---------------------------------------------------------------------------
302// CspStats
303// ---------------------------------------------------------------------------
304
305/// Summary statistics about the CSP instance.
306#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct CspStats {
308    /// Number of variables registered.
309    pub num_variables: usize,
310    /// Number of constraints registered.
311    pub num_constraints: usize,
312    /// Sum of all domain sizes.
313    pub total_domain_size: usize,
314}
315
316// ---------------------------------------------------------------------------
317// ConstraintSolver
318// ---------------------------------------------------------------------------
319
320/// A full-featured CSP solver combining AC-3 domain pruning with
321/// backtracking search and configurable variable/value ordering heuristics.
322pub struct ConstraintSolver {
323    /// All registered variables (in insertion order).
324    pub variables: Vec<CspVariable>,
325    /// All registered constraints.
326    pub constraints: Vec<Constraint>,
327    /// Solver configuration.
328    pub config: SolverConfig,
329}
330
331impl ConstraintSolver {
332    // -----------------------------------------------------------------------
333    // Construction
334    // -----------------------------------------------------------------------
335
336    /// Create a new, empty solver with the given configuration.
337    pub fn new(config: SolverConfig) -> Self {
338        Self {
339            variables: Vec::new(),
340            constraints: Vec::new(),
341            config,
342        }
343    }
344
345    /// Register a new variable with the given name and initial domain values.
346    ///
347    /// Returns the `CspVarId` that uniquely identifies this variable.
348    pub fn add_variable(&mut self, name: String, domain: Vec<i64>) -> CspVarId {
349        let id = CspVarId(self.variables.len());
350        self.variables.push(CspVariable {
351            id,
352            name,
353            domain: Domain::new(domain),
354        });
355        id
356    }
357
358    /// Append a constraint to the problem.
359    pub fn add_constraint(&mut self, c: Constraint) {
360        self.constraints.push(c);
361    }
362
363    // -----------------------------------------------------------------------
364    // Statistics
365    // -----------------------------------------------------------------------
366
367    /// Return a summary of the current CSP instance.
368    pub fn stats(&self) -> CspStats {
369        CspStats {
370            num_variables: self.variables.len(),
371            num_constraints: self.constraints.len(),
372            total_domain_size: self.variables.iter().map(|v| v.domain.len()).sum(),
373        }
374    }
375
376    // -----------------------------------------------------------------------
377    // Consistency checking
378    // -----------------------------------------------------------------------
379
380    /// Return `true` when all constraints that are fully grounded by
381    /// `assignment` (plus the tentative assignment of `value` to `var`) are
382    /// satisfied.
383    pub fn is_consistent(&self, assignment: &Assignment, var: CspVarId, value: i64) -> bool {
384        self.constraints
385            .iter()
386            .all(|c| self.check_constraint(c, assignment, var, value))
387    }
388
389    /// Check a single constraint under a partial assignment with `var = value`.
390    ///
391    /// Returns `true` when:
392    /// - the constraint cannot yet be evaluated (some participant is unassigned), OR
393    /// - the constraint is satisfied by the current (partial+tentative) assignment.
394    pub fn check_constraint(
395        &self,
396        c: &Constraint,
397        assignment: &Assignment,
398        var: CspVarId,
399        value: i64,
400    ) -> bool {
401        // Build a temporary lookup closure that merges the existing assignment
402        // with the tentative (var = value).
403        let get = |v: CspVarId| -> Option<i64> {
404            if v == var {
405                Some(value)
406            } else {
407                assignment.get(v)
408            }
409        };
410
411        match c {
412            Constraint::Equal(a, b) => match (get(*a), get(*b)) {
413                (Some(va), Some(vb)) => va == vb,
414                _ => true,
415            },
416            Constraint::NotEqual(a, b) => match (get(*a), get(*b)) {
417                (Some(va), Some(vb)) => va != vb,
418                _ => true,
419            },
420            Constraint::LessThan(a, b) => match (get(*a), get(*b)) {
421                (Some(va), Some(vb)) => va < vb,
422                _ => true,
423            },
424            Constraint::LessEqual(a, b) => match (get(*a), get(*b)) {
425                (Some(va), Some(vb)) => va <= vb,
426                _ => true,
427            },
428            Constraint::AllDifferent(vars) => {
429                // Collect all currently-assigned values for participants.
430                let mut seen: Vec<i64> = Vec::with_capacity(vars.len());
431                for &v in vars {
432                    if let Some(val) = get(v) {
433                        if seen.contains(&val) {
434                            return false;
435                        }
436                        seen.push(val);
437                    }
438                }
439                true
440            }
441            Constraint::Sum { vars, target } => {
442                // Only evaluate when all participants are assigned.
443                let mut total: i64 = 0;
444                let mut all_assigned = true;
445                let mut partial_sum: i64 = 0;
446                let mut partial_count = 0;
447                for &v in vars {
448                    match get(v) {
449                        Some(val) => {
450                            total += val;
451                            partial_sum += val;
452                            partial_count += 1;
453                        }
454                        None => {
455                            all_assigned = false;
456                        }
457                    }
458                }
459                let _ = (total, partial_sum, partial_count);
460                if all_assigned {
461                    // All assigned: check exact sum
462                    let mut s: i64 = 0;
463                    for &v in vars {
464                        s += get(v).unwrap_or(0);
465                    }
466                    s == *target
467                } else {
468                    // Partial: only prune if partial sum already exceeds target
469                    // (assuming non-negative domains for simple pruning)
470                    let partial: i64 = vars.iter().filter_map(|&v| get(v)).sum();
471                    partial <= *target
472                }
473            }
474            Constraint::InDomain { var: dvar, allowed } => {
475                if let Some(val) = get(*dvar) {
476                    allowed.contains(&val)
477                } else {
478                    true
479                }
480            }
481        }
482    }
483
484    // -----------------------------------------------------------------------
485    // AC-3
486    // -----------------------------------------------------------------------
487
488    /// Run the AC-3 arc-consistency algorithm on the given `domains` (which
489    /// are cloned from the initial variable domains at the start of `solve`).
490    ///
491    /// Returns `false` when arc consistency reveals that the problem has no
492    /// solution (some domain became empty).
493    pub fn ac3(&self, domains: &mut [Domain]) -> bool {
494        // Seed the worklist with every (xi, xj) pair implied by a constraint.
495        let mut worklist: VecDeque<(CspVarId, CspVarId)> = VecDeque::new();
496
497        for c in &self.constraints {
498            match c {
499                Constraint::Equal(a, b)
500                | Constraint::NotEqual(a, b)
501                | Constraint::LessThan(a, b)
502                | Constraint::LessEqual(a, b) => {
503                    worklist.push_back((*a, *b));
504                    worklist.push_back((*b, *a));
505                }
506                Constraint::AllDifferent(vars) => {
507                    for i in 0..vars.len() {
508                        for j in 0..vars.len() {
509                            if i != j {
510                                worklist.push_back((vars[i], vars[j]));
511                            }
512                        }
513                    }
514                }
515                // InDomain is a unary constraint — prune immediately.
516                Constraint::InDomain { var, allowed } => {
517                    let idx = var.0;
518                    if idx < domains.len() {
519                        domains[idx].values.retain(|v| allowed.contains(v));
520                        if domains[idx].is_empty() {
521                            return false;
522                        }
523                    }
524                }
525                // Sum has no efficient binary-arc encoding; skip for AC-3.
526                Constraint::Sum { .. } => {}
527            }
528        }
529
530        while let Some((xi, xj)) = worklist.pop_front() {
531            if self.revise(domains, xi, xj) {
532                if domains.get(xi.0).is_none_or(Domain::is_empty) {
533                    return false;
534                }
535                // Re-add arcs from neighbours of xi (excluding xj) back to xi.
536                let neighbours = self.neighbours_of(xi);
537                for xk in neighbours {
538                    if xk != xj {
539                        worklist.push_back((xk, xi));
540                    }
541                }
542            }
543        }
544
545        true
546    }
547
548    /// Revise `xi`'s domain: remove any value that has no support in `xj`.
549    ///
550    /// Returns `true` when at least one value was removed from `xi`'s domain.
551    pub fn revise(&self, domains: &mut [Domain], xi: CspVarId, xj: CspVarId) -> bool {
552        if xi.0 >= domains.len() || xj.0 >= domains.len() {
553            return false;
554        }
555
556        // Snapshot the current domain of xj to avoid borrow conflicts.
557        let xj_vals: Vec<i64> = domains[xj.0].values.clone();
558        let xi_vals: Vec<i64> = domains[xi.0].values.clone();
559
560        // Collect only binary constraints between xi and xj.
561        let relevant: Vec<&Constraint> = self
562            .constraints
563            .iter()
564            .filter(|c| c.involves_pair(xi, xj))
565            .collect();
566
567        let mut to_remove: Vec<i64> = Vec::new();
568        for &vxi in &xi_vals {
569            let has_support = xj_vals.iter().any(|&vyj| {
570                relevant.iter().all(|c| {
571                    let mut tmp = Assignment::new();
572                    tmp.set(xi, vxi);
573                    tmp.set(xj, vyj);
574                    // Use an empty "var/value" to check with the full assignment.
575                    // We create a dummy CspVarId that won't collide with xi/xj.
576                    self.check_constraint(c, &tmp, CspVarId(usize::MAX), 0)
577                })
578            });
579            if !has_support {
580                to_remove.push(vxi);
581            }
582        }
583
584        if to_remove.is_empty() {
585            return false;
586        }
587        for v in &to_remove {
588            domains[xi.0].remove(*v);
589        }
590        true
591    }
592
593    /// Collect the set of variable ids that share any binary constraint with `var`.
594    fn neighbours_of(&self, var: CspVarId) -> Vec<CspVarId> {
595        let mut result: Vec<CspVarId> = Vec::new();
596        for c in &self.constraints {
597            match c {
598                Constraint::Equal(a, b)
599                | Constraint::NotEqual(a, b)
600                | Constraint::LessThan(a, b)
601                | Constraint::LessEqual(a, b) => {
602                    if *a == var && !result.contains(b) {
603                        result.push(*b);
604                    } else if *b == var && !result.contains(a) {
605                        result.push(*a);
606                    }
607                }
608                Constraint::AllDifferent(vars) => {
609                    if vars.contains(&var) {
610                        for &v in vars {
611                            if v != var && !result.contains(&v) {
612                                result.push(v);
613                            }
614                        }
615                    }
616                }
617                Constraint::Sum { vars, .. } => {
618                    if vars.contains(&var) {
619                        for &v in vars {
620                            if v != var && !result.contains(&v) {
621                                result.push(v);
622                            }
623                        }
624                    }
625                }
626                Constraint::InDomain { .. } => {}
627            }
628        }
629        result
630    }
631
632    // -----------------------------------------------------------------------
633    // Variable and value ordering heuristics
634    // -----------------------------------------------------------------------
635
636    /// Select the next unassigned variable to expand.
637    ///
638    /// When `use_mrv` is enabled, picks the variable with the fewest remaining
639    /// domain values (ties broken by variable id). Otherwise returns the first
640    /// unassigned variable in insertion order.
641    pub fn select_unassigned_variable(
642        &self,
643        assignment: &Assignment,
644        domains: &[Domain],
645    ) -> Option<CspVarId> {
646        let unassigned: Vec<CspVarId> = self
647            .variables
648            .iter()
649            .map(|v| v.id)
650            .filter(|id| assignment.get(*id).is_none())
651            .collect();
652
653        if unassigned.is_empty() {
654            return None;
655        }
656
657        if self.config.use_mrv {
658            unassigned.into_iter().min_by_key(|id| {
659                let size = domains.get(id.0).map_or(0, Domain::len);
660                // Use (size, id) for deterministic tie-breaking.
661                (size, id.0)
662            })
663        } else {
664            unassigned.into_iter().next()
665        }
666    }
667
668    /// Return the values from `var`'s current domain in the order we should
669    /// try them during backtracking.
670    ///
671    /// When `use_lcv` is enabled this would apply the Least Constraining Value
672    /// heuristic; currently we return values in sorted (ascending) order as a
673    /// lightweight deterministic approximation.
674    pub fn order_domain_values(
675        &self,
676        var: CspVarId,
677        _assignment: &Assignment,
678        domains: &[Domain],
679    ) -> Vec<i64> {
680        domains
681            .get(var.0)
682            .map_or_else(Vec::new, |d| d.values.clone())
683    }
684
685    // -----------------------------------------------------------------------
686    // Backtracking search
687    // -----------------------------------------------------------------------
688
689    /// Recursive backtracking search.
690    ///
691    /// Returns `true` when the search should stop (either `max_solutions`
692    /// reached or `max_backtracks` exceeded).
693    pub fn backtrack(
694        &self,
695        assignment: &mut Assignment,
696        domains: &mut Vec<Domain>,
697        result: &mut SolverResult,
698    ) -> bool {
699        // Check termination conditions.
700        if self.config.max_backtracks > 0 && result.backtracks >= self.config.max_backtracks as u64
701        {
702            return true;
703        }
704
705        // All variables assigned → record solution.
706        if assignment.is_complete(self.variables.len()) {
707            result.solutions.push(assignment.clone());
708            return result.solutions.len() >= self.config.max_solutions;
709        }
710
711        // Choose next variable.
712        let var = match self.select_unassigned_variable(assignment, domains) {
713            Some(v) => v,
714            None => return false,
715        };
716
717        let ordered_values = self.order_domain_values(var, assignment, domains);
718
719        for value in ordered_values {
720            result.constraint_checks += 1;
721            if self.is_consistent(assignment, var, value) {
722                // Assign and recurse.
723                assignment.set(var, value);
724
725                // Forward-check: ensure no neighbour's domain becomes empty.
726                let domains_backup: Vec<Domain> = domains.clone();
727                let mut fc_ok = true;
728                for neighbour in self.neighbours_of(var) {
729                    if assignment.get(neighbour).is_some() {
730                        continue;
731                    }
732                    let orig: Vec<i64> = domains[neighbour.0].values.clone();
733                    let pruned: Vec<i64> = orig
734                        .into_iter()
735                        .filter(|&nv| self.is_consistent(assignment, neighbour, nv))
736                        .collect();
737                    domains[neighbour.0].values = pruned;
738                    if domains[neighbour.0].is_empty() {
739                        fc_ok = false;
740                        break;
741                    }
742                }
743
744                if fc_ok && self.backtrack(assignment, domains, result) {
745                    return true;
746                }
747
748                // Undo forward-checking domain reductions and the assignment.
749                *domains = domains_backup;
750                assignment.unset(var);
751                result.backtracks += 1;
752            }
753        }
754
755        false
756    }
757
758    // -----------------------------------------------------------------------
759    // Top-level solve
760    // -----------------------------------------------------------------------
761
762    /// Solve the CSP and return a [`SolverResult`] with all found solutions
763    /// and search statistics.
764    pub fn solve(&mut self) -> SolverResult {
765        let start = Instant::now();
766        let mut result = SolverResult::new();
767
768        // Clone initial domains for the search.
769        let mut domains: Vec<Domain> = self.variables.iter().map(|v| v.domain.clone()).collect();
770
771        // Optionally run AC-3 to prune domains.
772        if self.config.use_ac3 && !self.ac3(&mut domains) {
773            result.time_ms = start.elapsed().as_millis() as u64;
774            return result; // no solutions (AC-3 proved unsatisfiable)
775        }
776
777        // Bail early if any domain is already empty.
778        if domains.iter().any(Domain::is_empty) {
779            result.time_ms = start.elapsed().as_millis() as u64;
780            return result;
781        }
782
783        let mut assignment = Assignment::new();
784        self.backtrack(&mut assignment, &mut domains, &mut result);
785
786        result.time_ms = start.elapsed().as_millis() as u64;
787        result
788    }
789}
790
791// ---------------------------------------------------------------------------
792// Tests
793// ---------------------------------------------------------------------------
794
795#[cfg(test)]
796mod tests {
797    use crate::constraint_solver::{
798        Assignment, Constraint, ConstraintSolver, CspVarId, Domain, SolverConfig, SolverResult,
799    };
800
801    fn default_solver() -> ConstraintSolver {
802        ConstraintSolver::new(SolverConfig::default())
803    }
804
805    // -----------------------------------------------------------------------
806    // Domain tests
807    // -----------------------------------------------------------------------
808
809    #[test]
810    fn test_domain_new_sorts_and_deduplicates() {
811        let d = Domain::new(vec![3, 1, 2, 1, 3]);
812        assert_eq!(d.values, vec![1, 2, 3]);
813    }
814
815    #[test]
816    fn test_domain_is_empty_when_no_values() {
817        assert!(Domain::new(vec![]).is_empty());
818        assert!(!Domain::new(vec![1]).is_empty());
819    }
820
821    #[test]
822    fn test_domain_contains() {
823        let d = Domain::new(vec![10, 20, 30]);
824        assert!(d.contains(10));
825        assert!(d.contains(20));
826        assert!(!d.contains(5));
827        assert!(!d.contains(99));
828    }
829
830    #[test]
831    fn test_domain_remove_present_value() {
832        let mut d = Domain::new(vec![1, 2, 3]);
833        let removed = d.remove(2);
834        assert!(removed);
835        assert_eq!(d.values, vec![1, 3]);
836    }
837
838    #[test]
839    fn test_domain_remove_absent_value_is_noop() {
840        let mut d = Domain::new(vec![1, 2, 3]);
841        let removed = d.remove(99);
842        assert!(!removed);
843        assert_eq!(d.values, vec![1, 2, 3]);
844    }
845
846    #[test]
847    fn test_domain_len() {
848        assert_eq!(Domain::new(vec![1, 2, 3]).len(), 3);
849        assert_eq!(Domain::new(vec![]).len(), 0);
850    }
851
852    // -----------------------------------------------------------------------
853    // Assignment tests
854    // -----------------------------------------------------------------------
855
856    #[test]
857    fn test_assignment_set_and_get() {
858        let mut a = Assignment::new();
859        let v = CspVarId(0);
860        assert_eq!(a.get(v), None);
861        a.set(v, 42);
862        assert_eq!(a.get(v), Some(42));
863    }
864
865    #[test]
866    fn test_assignment_unset() {
867        let mut a = Assignment::new();
868        let v = CspVarId(0);
869        a.set(v, 7);
870        a.unset(v);
871        assert_eq!(a.get(v), None);
872    }
873
874    #[test]
875    fn test_assignment_is_complete() {
876        let mut a = Assignment::new();
877        assert!(!a.is_complete(2));
878        a.set(CspVarId(0), 1);
879        a.set(CspVarId(1), 2);
880        assert!(a.is_complete(2));
881    }
882
883    // -----------------------------------------------------------------------
884    // Solver construction
885    // -----------------------------------------------------------------------
886
887    #[test]
888    fn test_add_variable_returns_sequential_ids() {
889        let mut solver = default_solver();
890        let x = solver.add_variable("x".to_string(), vec![1, 2]);
891        let y = solver.add_variable("y".to_string(), vec![3, 4]);
892        assert_eq!(x, CspVarId(0));
893        assert_eq!(y, CspVarId(1));
894    }
895
896    #[test]
897    fn test_stats() {
898        let mut solver = default_solver();
899        solver.add_variable("a".to_string(), vec![1, 2, 3]);
900        solver.add_variable("b".to_string(), vec![4, 5]);
901        let s = solver.stats();
902        assert_eq!(s.num_variables, 2);
903        assert_eq!(s.total_domain_size, 5);
904        assert_eq!(s.num_constraints, 0);
905    }
906
907    // -----------------------------------------------------------------------
908    // Simple satisfiable cases
909    // -----------------------------------------------------------------------
910
911    #[test]
912    fn test_no_constraints_always_satisfiable() {
913        let mut solver = default_solver();
914        let _x = solver.add_variable("x".to_string(), vec![42]);
915        let result = solver.solve();
916        assert_eq!(result.solutions.len(), 1);
917        assert_eq!(result.solutions[0].get(CspVarId(0)), Some(42));
918    }
919
920    #[test]
921    fn test_not_equal_two_vars_satisfiable() {
922        let mut solver = default_solver();
923        let x = solver.add_variable("x".to_string(), vec![1, 2]);
924        let y = solver.add_variable("y".to_string(), vec![1, 2]);
925        solver.add_constraint(Constraint::NotEqual(x, y));
926        let result = solver.solve();
927        assert_eq!(result.solutions.len(), 1);
928        let sol = &result.solutions[0];
929        assert_ne!(sol.get(x), sol.get(y));
930    }
931
932    #[test]
933    fn test_equal_constraint() {
934        let mut solver = default_solver();
935        let x = solver.add_variable("x".to_string(), vec![1, 2, 3]);
936        let y = solver.add_variable("y".to_string(), vec![2, 3, 4]);
937        solver.add_constraint(Constraint::Equal(x, y));
938        let result = solver.solve();
939        assert!(!result.solutions.is_empty());
940        let sol = &result.solutions[0];
941        assert_eq!(sol.get(x), sol.get(y));
942    }
943
944    #[test]
945    fn test_less_than_constraint() {
946        let mut solver = default_solver();
947        let a = solver.add_variable("a".to_string(), vec![1, 2, 3]);
948        let b = solver.add_variable("b".to_string(), vec![1, 2, 3]);
949        solver.add_constraint(Constraint::LessThan(a, b));
950        let result = solver.solve();
951        assert!(!result.solutions.is_empty());
952        let sol = &result.solutions[0];
953        assert!(
954            sol.get(a).expect("test: should succeed") < sol.get(b).expect("test: should succeed")
955        );
956    }
957
958    #[test]
959    fn test_less_equal_constraint() {
960        let mut solver = default_solver();
961        let a = solver.add_variable("a".to_string(), vec![5]);
962        let b = solver.add_variable("b".to_string(), vec![5]);
963        solver.add_constraint(Constraint::LessEqual(a, b));
964        let result = solver.solve();
965        assert_eq!(result.solutions.len(), 1);
966        let sol = &result.solutions[0];
967        assert!(
968            sol.get(a).expect("test: should succeed") <= sol.get(b).expect("test: should succeed")
969        );
970    }
971
972    #[test]
973    fn test_all_different_three_vars() {
974        let mut solver = default_solver();
975        let x = solver.add_variable("x".to_string(), vec![1, 2, 3]);
976        let y = solver.add_variable("y".to_string(), vec![1, 2, 3]);
977        let z = solver.add_variable("z".to_string(), vec![1, 2, 3]);
978        solver.add_constraint(Constraint::AllDifferent(vec![x, y, z]));
979        let result = solver.solve();
980        assert_eq!(result.solutions.len(), 1);
981        let sol = &result.solutions[0];
982        assert_ne!(sol.get(x), sol.get(y));
983        assert_ne!(sol.get(x), sol.get(z));
984        assert_ne!(sol.get(y), sol.get(z));
985    }
986
987    #[test]
988    fn test_in_domain_constraint() {
989        let mut solver = default_solver();
990        let x = solver.add_variable("x".to_string(), vec![1, 2, 3, 4, 5]);
991        solver.add_constraint(Constraint::InDomain {
992            var: x,
993            allowed: vec![2, 4],
994        });
995        let result = solver.solve();
996        assert_eq!(result.solutions.len(), 1);
997        let val = result.solutions[0].get(x).unwrap_or(-1);
998        assert!(val == 2 || val == 4);
999    }
1000
1001    #[test]
1002    fn test_sum_constraint_exact() {
1003        let mut solver = ConstraintSolver::new(SolverConfig {
1004            use_ac3: false, // skip AC-3 for sum (not fully supported)
1005            ..SolverConfig::default()
1006        });
1007        let a = solver.add_variable("a".to_string(), vec![1, 2, 3]);
1008        let b = solver.add_variable("b".to_string(), vec![1, 2, 3]);
1009        solver.add_constraint(Constraint::Sum {
1010            vars: vec![a, b],
1011            target: 4,
1012        });
1013        let result = solver.solve();
1014        assert!(!result.solutions.is_empty());
1015        let sol = &result.solutions[0];
1016        assert_eq!(sol.get(a).unwrap_or(0) + sol.get(b).unwrap_or(0), 4);
1017    }
1018
1019    // -----------------------------------------------------------------------
1020    // Unsatisfiable cases
1021    // -----------------------------------------------------------------------
1022
1023    #[test]
1024    fn test_empty_domain_unsatisfiable() {
1025        let mut solver = default_solver();
1026        solver.add_variable("x".to_string(), vec![]);
1027        let result = solver.solve();
1028        assert!(result.solutions.is_empty());
1029    }
1030
1031    #[test]
1032    fn test_not_equal_single_value_unsatisfiable() {
1033        let mut solver = default_solver();
1034        let x = solver.add_variable("x".to_string(), vec![5]);
1035        let y = solver.add_variable("y".to_string(), vec![5]);
1036        solver.add_constraint(Constraint::NotEqual(x, y));
1037        let result = solver.solve();
1038        assert!(result.solutions.is_empty());
1039    }
1040
1041    #[test]
1042    fn test_all_different_too_few_values_unsatisfiable() {
1043        let mut solver = default_solver();
1044        let x = solver.add_variable("x".to_string(), vec![1, 2]);
1045        let y = solver.add_variable("y".to_string(), vec![1, 2]);
1046        let z = solver.add_variable("z".to_string(), vec![1, 2]);
1047        solver.add_constraint(Constraint::AllDifferent(vec![x, y, z]));
1048        let result = solver.solve();
1049        assert!(result.solutions.is_empty());
1050    }
1051
1052    #[test]
1053    fn test_less_than_equal_values_unsatisfiable() {
1054        let mut solver = default_solver();
1055        let a = solver.add_variable("a".to_string(), vec![5]);
1056        let b = solver.add_variable("b".to_string(), vec![5]);
1057        solver.add_constraint(Constraint::LessThan(a, b));
1058        let result = solver.solve();
1059        assert!(result.solutions.is_empty());
1060    }
1061
1062    #[test]
1063    fn test_equal_disjoint_domains_unsatisfiable() {
1064        let mut solver = default_solver();
1065        let x = solver.add_variable("x".to_string(), vec![1, 2]);
1066        let y = solver.add_variable("y".to_string(), vec![3, 4]);
1067        solver.add_constraint(Constraint::Equal(x, y));
1068        let result = solver.solve();
1069        assert!(result.solutions.is_empty());
1070    }
1071
1072    #[test]
1073    fn test_in_domain_no_overlap_unsatisfiable() {
1074        let mut solver = default_solver();
1075        let x = solver.add_variable("x".to_string(), vec![1, 2, 3]);
1076        solver.add_constraint(Constraint::InDomain {
1077            var: x,
1078            allowed: vec![7, 8, 9],
1079        });
1080        let result = solver.solve();
1081        assert!(result.solutions.is_empty());
1082    }
1083
1084    // -----------------------------------------------------------------------
1085    // Multiple solutions
1086    // -----------------------------------------------------------------------
1087
1088    #[test]
1089    fn test_find_all_solutions() {
1090        let mut solver = ConstraintSolver::new(SolverConfig {
1091            max_solutions: 100,
1092            use_ac3: false,
1093            ..SolverConfig::default()
1094        });
1095        let x = solver.add_variable("x".to_string(), vec![1, 2, 3]);
1096        let y = solver.add_variable("y".to_string(), vec![1, 2, 3]);
1097        solver.add_constraint(Constraint::NotEqual(x, y));
1098        let result = solver.solve();
1099        // 3*3 - 3 = 6 solutions (x != y)
1100        assert_eq!(result.solutions.len(), 6);
1101        for sol in &result.solutions {
1102            assert_ne!(sol.get(x), sol.get(y));
1103        }
1104    }
1105
1106    // -----------------------------------------------------------------------
1107    // AC-3 tests
1108    // -----------------------------------------------------------------------
1109
1110    #[test]
1111    fn test_ac3_prunes_domain_for_not_equal() {
1112        let mut solver = ConstraintSolver::new(SolverConfig {
1113            use_ac3: true,
1114            ..SolverConfig::default()
1115        });
1116        // x ∈ {5}, y ∈ {5, 6}, x != y → AC-3 must prune y to {6}.
1117        let x = solver.add_variable("x".to_string(), vec![5]);
1118        let y = solver.add_variable("y".to_string(), vec![5, 6]);
1119        solver.add_constraint(Constraint::NotEqual(x, y));
1120
1121        let mut domains: Vec<Domain> = solver.variables.iter().map(|v| v.domain.clone()).collect();
1122        let consistent = solver.ac3(&mut domains);
1123        assert!(consistent);
1124        assert_eq!(domains[y.0].values, vec![6]);
1125    }
1126
1127    #[test]
1128    fn test_ac3_detects_unsatisfiability() {
1129        let mut solver = ConstraintSolver::new(SolverConfig {
1130            use_ac3: true,
1131            ..SolverConfig::default()
1132        });
1133        let x = solver.add_variable("x".to_string(), vec![5]);
1134        let y = solver.add_variable("y".to_string(), vec![5]);
1135        solver.add_constraint(Constraint::NotEqual(x, y));
1136
1137        let mut domains: Vec<Domain> = solver.variables.iter().map(|v| v.domain.clone()).collect();
1138        let consistent = solver.ac3(&mut domains);
1139        assert!(!consistent);
1140    }
1141
1142    #[test]
1143    fn test_ac3_prunes_less_than() {
1144        let mut solver = ConstraintSolver::new(SolverConfig {
1145            use_ac3: true,
1146            ..SolverConfig::default()
1147        });
1148        // a ∈ {3,4,5}, b ∈ {1,2,3}, a < b → nothing in a is < 1, max of b is 3.
1149        // AC-3 should prune a to {1,2} (values < max(b)=3) and b to {2,3}
1150        // (values > min(a)=3) — but since a ∈ {3,4,5} and b ≤ 3, a must be < b ≤ 3,
1151        // which is impossible → domains may collapse.
1152        let a = solver.add_variable("a".to_string(), vec![3, 4, 5]);
1153        let b = solver.add_variable("b".to_string(), vec![1, 2, 3]);
1154        solver.add_constraint(Constraint::LessThan(a, b));
1155        let result = solver.solve();
1156        // No solution possible since min(a)=3, max(b)=3, requires a < b.
1157        assert!(result.solutions.is_empty());
1158    }
1159
1160    #[test]
1161    fn test_ac3_in_domain_pruning() {
1162        let mut solver = default_solver();
1163        let x = solver.add_variable("x".to_string(), vec![1, 2, 3, 4, 5]);
1164        solver.add_constraint(Constraint::InDomain {
1165            var: x,
1166            allowed: vec![3],
1167        });
1168        let mut domains: Vec<Domain> = solver.variables.iter().map(|v| v.domain.clone()).collect();
1169        let ok = solver.ac3(&mut domains);
1170        assert!(ok);
1171        assert_eq!(domains[x.0].values, vec![3]);
1172    }
1173
1174    // -----------------------------------------------------------------------
1175    // Heuristics
1176    // -----------------------------------------------------------------------
1177
1178    #[test]
1179    fn test_mrv_selects_smallest_domain() {
1180        let mut solver = ConstraintSolver::new(SolverConfig {
1181            use_mrv: true,
1182            ..SolverConfig::default()
1183        });
1184        let x = solver.add_variable("x".to_string(), vec![1, 2, 3]);
1185        let y = solver.add_variable("y".to_string(), vec![1]);
1186        let domains: Vec<Domain> = solver.variables.iter().map(|v| v.domain.clone()).collect();
1187        let assignment = Assignment::new();
1188        let chosen = solver
1189            .select_unassigned_variable(&assignment, &domains)
1190            .expect("test: should succeed");
1191        // y has the smaller domain {1} so MRV should choose y.
1192        assert_eq!(chosen, y);
1193        let _ = x;
1194    }
1195
1196    #[test]
1197    fn test_no_mrv_selects_first() {
1198        let mut solver = ConstraintSolver::new(SolverConfig {
1199            use_mrv: false,
1200            ..SolverConfig::default()
1201        });
1202        let x = solver.add_variable("x".to_string(), vec![1, 2, 3]);
1203        let _y = solver.add_variable("y".to_string(), vec![1]);
1204        let domains: Vec<Domain> = solver.variables.iter().map(|v| v.domain.clone()).collect();
1205        let assignment = Assignment::new();
1206        let chosen = solver
1207            .select_unassigned_variable(&assignment, &domains)
1208            .expect("test: should succeed");
1209        // Without MRV, picks first unassigned (insertion order).
1210        assert_eq!(chosen, x);
1211    }
1212
1213    #[test]
1214    fn test_order_domain_values_returns_sorted() {
1215        let mut solver = default_solver();
1216        let x = solver.add_variable("x".to_string(), vec![5, 3, 1, 4, 2]);
1217        let domains: Vec<Domain> = solver.variables.iter().map(|v| v.domain.clone()).collect();
1218        let vals = solver.order_domain_values(x, &Assignment::new(), &domains);
1219        assert_eq!(vals, vec![1, 2, 3, 4, 5]);
1220    }
1221
1222    // -----------------------------------------------------------------------
1223    // Complex / integration tests
1224    // -----------------------------------------------------------------------
1225
1226    #[test]
1227    fn test_chained_less_than() {
1228        let mut solver = default_solver();
1229        let a = solver.add_variable("a".to_string(), vec![1, 2, 3]);
1230        let b = solver.add_variable("b".to_string(), vec![1, 2, 3]);
1231        let c = solver.add_variable("c".to_string(), vec![1, 2, 3]);
1232        solver.add_constraint(Constraint::LessThan(a, b));
1233        solver.add_constraint(Constraint::LessThan(b, c));
1234        let result = solver.solve();
1235        assert_eq!(result.solutions.len(), 1);
1236        let sol = &result.solutions[0];
1237        assert!(
1238            sol.get(a).expect("test: should succeed") < sol.get(b).expect("test: should succeed")
1239        );
1240        assert!(
1241            sol.get(b).expect("test: should succeed") < sol.get(c).expect("test: should succeed")
1242        );
1243    }
1244
1245    #[test]
1246    fn test_combined_all_different_and_less_than() {
1247        let mut solver = ConstraintSolver::new(SolverConfig {
1248            max_solutions: 10,
1249            ..SolverConfig::default()
1250        });
1251        let x = solver.add_variable("x".to_string(), vec![1, 2, 3, 4]);
1252        let y = solver.add_variable("y".to_string(), vec![1, 2, 3, 4]);
1253        let z = solver.add_variable("z".to_string(), vec![1, 2, 3, 4]);
1254        solver.add_constraint(Constraint::AllDifferent(vec![x, y, z]));
1255        solver.add_constraint(Constraint::LessThan(x, y));
1256        let result = solver.solve();
1257        assert!(!result.solutions.is_empty());
1258        for sol in &result.solutions {
1259            let vx = sol.get(x).expect("test: should succeed");
1260            let vy = sol.get(y).expect("test: should succeed");
1261            let vz = sol.get(z).expect("test: should succeed");
1262            assert_ne!(vx, vy);
1263            assert_ne!(vx, vz);
1264            assert_ne!(vy, vz);
1265            assert!(vx < vy);
1266        }
1267    }
1268
1269    #[test]
1270    fn test_single_variable_no_constraint() {
1271        let mut solver = default_solver();
1272        let x = solver.add_variable("x".to_string(), vec![7]);
1273        let result = solver.solve();
1274        assert_eq!(result.solutions.len(), 1);
1275        assert_eq!(result.solutions[0].get(x), Some(7));
1276    }
1277
1278    #[test]
1279    fn test_backtrack_count_increments() {
1280        let mut solver = ConstraintSolver::new(SolverConfig {
1281            max_solutions: 100,
1282            use_ac3: false,
1283            ..SolverConfig::default()
1284        });
1285        let x = solver.add_variable("x".to_string(), vec![1, 2]);
1286        let y = solver.add_variable("y".to_string(), vec![1, 2]);
1287        solver.add_constraint(Constraint::NotEqual(x, y));
1288        let result = solver.solve();
1289        // Some backtracks must have occurred.
1290        // We only assert the stat is present and non-negative (u64).
1291        assert!(result.backtracks < u64::MAX);
1292        let _ = result.constraint_checks;
1293    }
1294
1295    #[test]
1296    fn test_time_ms_is_set() {
1297        let mut solver = default_solver();
1298        solver.add_variable("x".to_string(), vec![1]);
1299        let result = solver.solve();
1300        // time_ms is u64; just verify it was populated (it can be 0 on fast machines).
1301        let _ = result.time_ms;
1302    }
1303
1304    #[test]
1305    fn test_constraint_involves() {
1306        let c = Constraint::NotEqual(CspVarId(0), CspVarId(1));
1307        assert!(c.involves(CspVarId(0)));
1308        assert!(c.involves(CspVarId(1)));
1309        assert!(!c.involves(CspVarId(2)));
1310    }
1311
1312    #[test]
1313    fn test_constraint_variables_all_different() {
1314        let c = Constraint::AllDifferent(vec![CspVarId(0), CspVarId(2), CspVarId(4)]);
1315        let vars = c.variables();
1316        assert!(vars.contains(&CspVarId(0)));
1317        assert!(vars.contains(&CspVarId(2)));
1318        assert!(vars.contains(&CspVarId(4)));
1319        assert_eq!(vars.len(), 3);
1320    }
1321
1322    #[test]
1323    fn test_solver_config_default() {
1324        let cfg = SolverConfig::default();
1325        assert_eq!(cfg.max_solutions, 1);
1326        assert!(cfg.use_ac3);
1327        assert!(cfg.use_mrv);
1328        assert!(!cfg.use_lcv);
1329        assert_eq!(cfg.max_backtracks, 100_000);
1330    }
1331
1332    #[test]
1333    fn test_less_equal_equal_values_satisfiable() {
1334        let mut solver = default_solver();
1335        let a = solver.add_variable("a".to_string(), vec![3, 4]);
1336        let b = solver.add_variable("b".to_string(), vec![3, 4]);
1337        solver.add_constraint(Constraint::LessEqual(a, b));
1338        let result = solver.solve();
1339        assert!(!result.solutions.is_empty());
1340        let sol = &result.solutions[0];
1341        assert!(
1342            sol.get(a).expect("test: should succeed") <= sol.get(b).expect("test: should succeed")
1343        );
1344    }
1345
1346    #[test]
1347    fn test_multiple_in_domain_constraints() {
1348        let mut solver = default_solver();
1349        let x = solver.add_variable("x".to_string(), vec![1, 2, 3, 4, 5]);
1350        solver.add_constraint(Constraint::InDomain {
1351            var: x,
1352            allowed: vec![2, 3, 4],
1353        });
1354        solver.add_constraint(Constraint::InDomain {
1355            var: x,
1356            allowed: vec![3, 4, 5],
1357        });
1358        let result = solver.solve();
1359        assert!(!result.solutions.is_empty());
1360        let val = result.solutions[0].get(x).unwrap_or(-1);
1361        // Intersection is {3, 4}.
1362        assert!(val == 3 || val == 4);
1363    }
1364
1365    #[test]
1366    fn test_solver_result_new() {
1367        let r = SolverResult::new();
1368        assert!(r.solutions.is_empty());
1369        assert_eq!(r.backtracks, 0);
1370        assert_eq!(r.constraint_checks, 0);
1371        assert_eq!(r.time_ms, 0);
1372    }
1373
1374    #[test]
1375    fn test_assignment_iter() {
1376        let mut a = Assignment::new();
1377        a.set(CspVarId(0), 10);
1378        a.set(CspVarId(1), 20);
1379        let mut collected: Vec<(CspVarId, i64)> = a.iter().collect();
1380        collected.sort_by_key(|(id, _)| id.0);
1381        assert_eq!(collected, vec![(CspVarId(0), 10), (CspVarId(1), 20)]);
1382    }
1383}