use csp_solver::assignment;
use csp_solver::constraint::{AllDifferent, AllDifferentExcept, Constraint, Revision};
use csp_solver::domain::{BitsetDomain, FiniteDomain};
use csp_solver::solver::gac::{next_gac_id, propagate_gac_core};
use csp_solver::variable::Variable;
use csp_solver::{Csp, SolveConfig};
fn vars_bit(sets: &[Vec<u32>]) -> Vec<Variable<BitsetDomain>> {
sets.iter()
.map(|s| Variable::new(BitsetDomain::new(s.iter().copied())))
.collect()
}
fn snapshot(vars: &[Variable<BitsetDomain>]) -> Vec<Vec<u32>> {
vars.iter()
.map(|v| {
let mut d: Vec<u32> = v.domain.iter().collect();
d.sort_unstable();
d
})
.collect()
}
fn run_plain(sets: &[Vec<u32>], gac_id: Option<u32>) -> (Revision, Vec<Vec<u32>>) {
let scope: Vec<u32> = (0..sets.len() as u32).collect();
let mut vars = vars_bit(sets);
let rev = propagate_gac_core::<BitsetDomain>(&scope, None, &mut vars, 0, gac_id);
(rev, snapshot(&vars))
}
#[test]
fn p1_warm_cold_pruning_parity_single_call() {
let batteries: Vec<Vec<Vec<u32>>> = vec![
vec![vec![0, 1], vec![0, 1], vec![0, 1, 2]],
vec![vec![0, 1], vec![0, 1], vec![2, 3], vec![0, 1, 2, 3]],
(0..9).map(|_| (0..9).collect()).collect(),
vec![vec![5, 9], vec![5, 9], vec![5, 9, 20]],
];
for sets in &batteries {
let (rev_cold, dom_cold) = run_plain(sets, None);
let (rev_warm, dom_warm) = run_plain(sets, Some(next_gac_id()));
assert_eq!(rev_cold, rev_warm, "revision parity for {sets:?}");
assert_eq!(dom_cold, dom_warm, "domain parity for {sets:?}");
}
}
#[test]
fn p1_warm_cold_parity_multi_call_universe_shrink() {
let warm_id = next_gac_id();
let steps: Vec<Vec<Vec<u32>>> = vec![
vec![
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
],
vec![
vec![0, 1],
vec![0, 1],
vec![0, 1, 2, 3, 4],
vec![2, 3, 4],
vec![2, 3, 4],
vec![2, 3, 4],
],
vec![
vec![0],
vec![1],
vec![2, 3, 4],
vec![2, 3, 4],
vec![2, 3, 4],
vec![2, 3, 4],
],
vec![
vec![0],
vec![1],
vec![2, 3],
vec![2, 3],
vec![2, 3, 4],
vec![4],
],
];
for (i, sets) in steps.iter().enumerate() {
let (rev_cold, dom_cold) = run_plain(sets, None);
let (rev_warm, dom_warm) = run_plain(sets, Some(warm_id));
assert_eq!(rev_cold, rev_warm, "step {i} revision parity");
assert_eq!(dom_cold, dom_warm, "step {i} domain parity (warm shrink)");
}
}
#[test]
fn p2_cross_universe_scratch_reset() {
let a: Vec<Vec<u32>> = vec![vec![1, 2], vec![1, 2], vec![1, 2, 15]];
let b: Vec<Vec<u32>> = vec![vec![100, 101], vec![100, 101], vec![100, 101, 108]];
let a2: Vec<Vec<u32>> = vec![vec![1, 2], vec![1, 2], vec![1, 2, 5]];
let same_a = run_plain(&a, Some(next_gac_id()));
let same_b = run_plain(&b, Some(next_gac_id()));
let same_a2 = run_plain(&a2, Some(next_gac_id()));
let fresh = |sets: Vec<Vec<u32>>| {
std::thread::spawn(move || run_plain(&sets, Some(next_gac_id())))
.join()
.unwrap()
};
assert_eq!(same_a, fresh(a), "universe A same==fresh");
assert_eq!(same_b, fresh(b), "universe B same==fresh");
assert_eq!(same_a2, fresh(a2), "shrunk universe A2 same==fresh");
assert_eq!(same_a.1[2], vec![15], "A: third var pruned to {{15}}");
assert_eq!(same_b.1[2], vec![108], "B: third var pruned to {{108}}");
assert_eq!(same_a2.1[2], vec![5], "A2: third var pruned to {{5}}");
}
#[test]
fn p3_generic_finite_domain_string_monomorphizes_and_solves() {
let colors: Vec<String> = ["red", "green", "blue", "yellow"]
.iter()
.map(|s| s.to_string())
.collect();
let mut csp: Csp<FiniteDomain<String>> = Csp::new();
let vars = csp.add_variables(&FiniteDomain::new(colors.clone()), 4);
csp.add_all_different(vars);
csp.finalize();
let config = SolveConfig {
max_solutions: usize::MAX,
..SolveConfig::default()
};
let solutions = csp.solve(&config);
assert_eq!(
solutions.len(),
24,
"4 colors, 4 all-different vars ⇒ 4! = 24"
);
for sol in &solutions {
let mut seen = sol.clone();
seen.sort();
seen.dedup();
assert_eq!(
seen.len(),
4,
"each solution is a permutation of 4 distinct colors"
);
}
}
#[test]
fn p4a_two_singletons_same_value_unsat() {
let ad = AllDifferent::new(vec![0, 1, 2]);
let mut vars = vars_bit(&[vec![5], vec![5], vec![5, 6, 7]]);
assert_eq!(ad.revise(&mut vars, 0), Revision::Unsatisfiable);
}
#[test]
fn p4b_snapshot_not_fuse_live() {
let ad = AllDifferent::new(vec![0, 1, 2]);
let mut vars = vars_bit(&[vec![0], vec![0, 1], vec![1, 2]]);
let rev = ad.revise(&mut vars, 0);
assert_eq!(rev, Revision::Changed);
assert_eq!(
snapshot(&vars),
vec![vec![0], vec![1], vec![1, 2]],
"C must retain {{1,2}} — snapshot semantics, not fuse-live"
);
}
#[test]
fn p4c_live_below_threshold_singleton_only() {
let ad = AllDifferent::new(vec![0, 1, 2]);
let mut vars = vars_bit(&[vec![2], vec![0, 1, 2], vec![2, 3]]);
let rev = ad.revise(&mut vars, 0);
assert_eq!(rev, Revision::Changed);
assert_eq!(snapshot(&vars), vec![vec![2], vec![0, 1], vec![3]]);
}
#[test]
fn p4d_all_different_except_small_scope_twin() {
let ade = AllDifferentExcept::new(vec![0, 1, 2], 0u32);
let mut vars = vars_bit(&[vec![2], vec![0, 2], vec![0]]);
let rev = ade.revise(&mut vars, 0);
assert_eq!(rev, Revision::Changed);
assert_eq!(
snapshot(&vars),
vec![vec![2], vec![0], vec![0]],
"sentinel peers keep 0; only the non-sentinel value 2 is removed"
);
let ade2 = AllDifferentExcept::new(vec![0, 1, 2], 0u32);
let mut vars2 = vars_bit(&[vec![3], vec![3], vec![3, 4]]);
assert_eq!(ade2.revise(&mut vars2, 0), Revision::Unsatisfiable);
}
fn lcg_cost_matrix(rows: usize, cols: usize, seed: u64) -> Vec<f64> {
let mut state = seed;
(0..rows * cols)
.map(|_| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 33) as f64) / (u32::MAX as f64) * 100.0
})
.collect()
}
fn solve_bnb(n: usize) -> csp_solver::AssignmentSolution {
let matrix = lcg_cost_matrix(n, n, 0xDEAD_BEEF);
assignment()
.rows(n)
.cols(n)
.cost(|i, k| matrix[i * n + k])
.unmatch_penalty(1000.0)
.solve_branch_and_bound()
.expect("solvable")
}
#[test]
fn p5_bnb_node_counts_frozen() {
let n10 = solve_bnb(10);
assert_eq!(n10.stats.nodes_explored, 506, "assign n=10 nodes (frozen)");
assert_eq!(n10.stats.backtracks, 515, "assign n=10 backtracks (frozen)");
assert!(!n10.stats.budget_exceeded);
let n15 = solve_bnb(15);
assert!(
!n15.stats.budget_exceeded,
"assign n=15 completes in budget"
);
assert!(
(3_000..5_000).contains(&n15.stats.nodes_explored),
"assign n=15 nodes {} outside the sane band 3_000..5_000 — a gross regression",
n15.stats.nodes_explored
);
let n20 = solve_bnb(20);
assert!(n20.stats.budget_exceeded, "n=20 B&B budget-blows");
assert_eq!(
n20.stats.nodes_explored, 1_000_000,
"n=20 halts at the deterministic node-budget cap"
);
}
#[test]
fn p5_n20_lap_proven_optimal() {
let n = 20;
let matrix = lcg_cost_matrix(n, n, 0xDEAD_BEEF);
let lap = assignment()
.rows(n)
.cols(n)
.cost(|i, k| matrix[i * n + k])
.unmatch_penalty(1000.0)
.solve()
.expect("solvable");
assert!(!lap.stats.budget_exceeded, "LAP path never budget-blows");
assert_eq!(lap.stats.nodes_explored, 0, "LAP path runs no search nodes");
let mut cols: Vec<i32> = lap.assign.clone();
assert!(
cols.iter().all(|&c| (0..n as i32).contains(&c)),
"all rows matched to real columns"
);
cols.sort_unstable();
cols.dedup();
assert_eq!(cols.len(), n, "columns are pairwise distinct");
assert!(
(lap.cost - 83.055652581).abs() < 1e-6,
"LAP optimal cost, got {}",
lap.cost
);
assert!(
lap.cost < 86.985140407,
"LAP beats the budget-blown B&B best-so-far"
);
}
#[test]
fn p6_sudoku_solves_under_pool() {
use csp_solver::sudoku::{Difficulty, generate_board, solve_sudoku};
use csp_solver::{Pruning, SolveConfig};
let board = generate_board(3, Difficulty::Easy);
let config = SolveConfig {
pruning: Pruning::Ac3,
..SolveConfig::default()
};
let sol = solve_sudoku(&board, 3, &config).expect("solvable 9×9");
assert_eq!(sol.len(), 81);
assert!(sol.iter().all(|&c| (1..=9).contains(&c)));
}
#[test]
fn p6_futoshiki_solves_under_pool() {
use csp_solver::puzzles::futoshiki::{FutoshikiPuzzle, solve_futoshiki};
let puzzle = FutoshikiPuzzle {
n: 5,
fixed_cells: vec![],
inequalities: vec![],
};
let solutions = solve_futoshiki(&puzzle);
assert!(!solutions.is_empty(), "5×5 Latin square is satisfiable");
assert_eq!(solutions[0].len(), 25);
}