use csp_solver::domain::CostFiniteDomain;
use csp_solver::domain::finite::FiniteDomain;
use csp_solver::domain::traits::Domain;
use csp_solver::ordering::Ordering;
use csp_solver::{Csp, OptimizationMode, Pruning, SolveConfig};
fn cost_domain(entries: Vec<(i32, f64)>) -> CostFiniteDomain {
let (values, costs): (Vec<i32>, Vec<f64>) = entries.into_iter().unzip();
CostFiniteDomain::new(values, costs)
}
#[test]
fn test_single_var_minimize() {
let mut csp = Csp::new();
let domain = cost_domain(vec![(1, 10.0), (2, 5.0), (3, 20.0)]);
let _x = csp.add_variable(domain);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::None,
ordering: Ordering::Chronological,
max_solutions: 1,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve_optimized(&config);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![2]);
}
#[test]
fn test_single_var_maximize() {
let mut csp = Csp::new();
let domain = cost_domain(vec![(1, 10.0), (2, 5.0), (3, 20.0)]);
let _x = csp.add_variable(domain);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::None,
ordering: Ordering::Chronological,
max_solutions: 1,
optimization_mode: OptimizationMode::MaximizeCost,
..Default::default()
};
let solutions = csp.solve_optimized(&config);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![3]);
}
#[test]
fn test_two_vars_not_equal_minimize() {
let mut csp = Csp::new();
let domain = cost_domain(vec![(0, 1.0), (1, 5.0)]);
let x = csp.add_variable(domain.clone());
let y = csp.add_variable(domain);
csp.add_not_equal(x, y);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
ordering: Ordering::FailFirst,
max_solutions: 10,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve_optimized(&config);
assert!(!solutions.is_empty());
let first = &solutions[0];
assert_ne!(first[0], first[1]);
}
#[test]
fn test_three_vars_minimize_with_constraints() {
let mut csp = Csp::new();
let dx = cost_domain(vec![(1, 10.0), (2, 1.0), (3, 5.0)]);
let dy = cost_domain(vec![(1, 3.0), (2, 8.0), (3, 2.0)]);
let dz = cost_domain(vec![(1, 7.0), (2, 4.0), (3, 1.0)]);
let x = csp.add_variable(dx);
let y = csp.add_variable(dy);
let z = csp.add_variable(dz);
csp.add_not_equal(x, y);
csp.add_not_equal(y, z);
csp.add_not_equal(x, z);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
ordering: Ordering::FailFirst,
max_solutions: 1,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve_optimized(&config);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![2, 1, 3]); }
#[test]
fn test_three_vars_maximize() {
let mut csp = Csp::new();
let dx = cost_domain(vec![(1, 10.0), (2, 1.0), (3, 5.0)]);
let dy = cost_domain(vec![(1, 3.0), (2, 8.0), (3, 2.0)]);
let dz = cost_domain(vec![(1, 7.0), (2, 4.0), (3, 1.0)]);
let x = csp.add_variable(dx);
let y = csp.add_variable(dy);
let z = csp.add_variable(dz);
csp.add_not_equal(x, y);
csp.add_not_equal(y, z);
csp.add_not_equal(x, z);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
ordering: Ordering::FailFirst,
max_solutions: 1,
optimization_mode: OptimizationMode::MaximizeCost,
..Default::default()
};
let solutions = csp.solve_optimized(&config);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![3, 2, 1]);
}
#[test]
fn test_feasibility_ignores_cost() {
let mut csp = Csp::new();
let domain = cost_domain(vec![(1, 100.0), (2, 0.0)]);
let _x = csp.add_variable(domain);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::None,
ordering: Ordering::Chronological,
max_solutions: 1,
optimization_mode: OptimizationMode::Feasibility,
..Default::default()
};
let solutions = csp.solve(&config);
assert_eq!(solutions.len(), 1);
}
#[test]
fn test_branch_and_bound_pruning() {
let mut csp = Csp::new();
let d0 = cost_domain(vec![(0, 0.0), (1, 100.0), (2, 200.0)]);
let d1 = cost_domain(vec![(0, 0.0), (1, 100.0), (2, 200.0)]);
let d2 = cost_domain(vec![(0, 0.0), (1, 100.0), (2, 200.0)]);
let d3 = cost_domain(vec![(0, 0.0), (1, 100.0), (2, 200.0)]);
let _v0 = csp.add_variable(d0);
let _v1 = csp.add_variable(d1);
let _v2 = csp.add_variable(d2);
let _v3 = csp.add_variable(d3);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::None,
ordering: Ordering::Chronological,
max_solutions: 1,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve_optimized(&config);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![0, 0, 0, 0]); }
#[test]
fn test_multiple_solutions_sorted() {
let mut csp = Csp::new();
let domain = cost_domain(vec![(1, 10.0), (2, 5.0), (3, 1.0)]);
let _x = csp.add_variable(domain);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::None,
ordering: Ordering::Chronological,
max_solutions: 10,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve_optimized(&config);
assert_eq!(solutions.len(), 3);
assert_eq!(solutions[0], vec![3]);
assert_eq!(solutions[1], vec![2]);
assert_eq!(solutions[2], vec![1]);
}
#[test]
fn test_solve_with_cost_eval_custom() {
use csp_solver::solver::optimize::DomainCostEval;
let mut csp: Csp<FiniteDomain<i32>> = Csp::new();
let domain = FiniteDomain::new(vec![1, 2, 3]);
let _x = csp.add_variable(domain);
csp.finalize();
struct SquareCost;
impl DomainCostEval<FiniteDomain<i32>> for SquareCost {
fn cost(&self, _domain: &FiniteDomain<i32>, val: &i32) -> f64 {
(*val as f64) * (*val as f64)
}
fn min_cost(&self, domain: &FiniteDomain<i32>) -> f64 {
domain
.values()
.into_iter()
.map(|v| (v as f64) * (v as f64))
.fold(f64::INFINITY, f64::min)
}
fn max_cost(&self, domain: &FiniteDomain<i32>) -> f64 {
domain
.values()
.into_iter()
.map(|v| (v as f64) * (v as f64))
.fold(f64::NEG_INFINITY, f64::max)
}
}
let config = SolveConfig {
pruning: Pruning::None,
ordering: Ordering::Chronological,
max_solutions: 1,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve_with_cost_eval(&config, &SquareCost);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![1]); }
#[test]
fn test_default_config_is_feasibility() {
let config = SolveConfig::default();
assert_eq!(config.optimization_mode, OptimizationMode::Feasibility);
}
#[test]
fn test_default_config_has_node_budget() {
let config = SolveConfig::default();
assert_eq!(config.node_budget, Some(1_000_000));
}
#[test]
fn test_node_budget_aborts_long_search() {
let mut csp = Csp::new();
let domain = cost_domain(vec![(1, 10.0), (2, 5.0), (3, 20.0), (4, 1.0), (5, 15.0)]);
let vars: Vec<_> = (0..30).map(|_| csp.add_variable(domain.clone())).collect();
for i in 0..vars.len() {
for j in (i + 1)..vars.len() {
csp.add_not_equal(vars[i], vars[j]);
}
}
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
ordering: Ordering::Chronological,
max_solutions: 1,
optimization_mode: OptimizationMode::MinimizeCost,
node_budget: Some(100),
..Default::default()
};
let _solutions = csp.solve_optimized(&config);
let stats = csp.stats();
assert!(
stats.budget_exceeded,
"budget guard did not fire (nodes_explored={})",
stats.nodes_explored
);
assert!(
stats.nodes_explored <= 101,
"explored {} nodes with a 100-node budget",
stats.nodes_explored
);
}
#[test]
fn test_node_budget_does_not_fire_for_small_search() {
let mut csp = Csp::new();
let domain = cost_domain(vec![(1, 10.0), (2, 5.0), (3, 20.0)]);
let _x = csp.add_variable(domain);
csp.finalize();
let config = SolveConfig {
pruning: Pruning::None,
ordering: Ordering::Chronological,
max_solutions: 1,
optimization_mode: OptimizationMode::MinimizeCost,
node_budget: Some(1_000),
..Default::default()
};
let solutions = csp.solve_optimized(&config);
let stats = csp.stats();
assert!(!stats.budget_exceeded);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![2]);
}