use csp_solver::constraint::SoftLambdaConstraint;
use csp_solver::domain::finite::FiniteDomain;
use csp_solver::domain::traits::{CostDomain, Domain};
use csp_solver::ordering::Ordering;
use csp_solver::{Csp, OptimizationMode, Pruning, SolveConfig};
#[derive(Debug, Clone, PartialEq)]
struct CostFiniteDomain {
active: Vec<i32>,
costs: Vec<(i32, f64)>,
}
impl CostFiniteDomain {
fn new(entries: Vec<(i32, f64)>) -> Self {
let active = entries.iter().map(|(v, _)| *v).collect();
Self {
active,
costs: entries,
}
}
fn cost_of(&self, val: &i32) -> f64 {
self.costs
.iter()
.find(|(v, _)| v == val)
.map(|(_, c)| *c)
.unwrap_or(0.0)
}
}
impl Domain for CostFiniteDomain {
type Value = i32;
fn size(&self) -> usize {
self.active.len()
}
fn contains(&self, val: &i32) -> bool {
self.active.contains(val)
}
fn remove(&mut self, val: &i32) -> bool {
if let Some(pos) = self.active.iter().position(|v| v == val) {
self.active.swap_remove(pos);
true
} else {
false
}
}
fn add(&mut self, val: &i32) {
if !self.active.contains(val) {
self.active.push(*val);
}
}
fn values(&self) -> Vec<i32> {
self.active.clone()
}
fn iter(&self) -> impl Iterator<Item = i32> {
self.active.clone().into_iter()
}
}
impl CostDomain for CostFiniteDomain {
fn cost(&self, val: &i32) -> f64 {
self.cost_of(val)
}
}
#[test]
fn test_single_var_minimize() {
let mut csp = Csp::new();
let domain = CostFiniteDomain::new(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 = CostFiniteDomain::new(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 = CostFiniteDomain::new(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 = CostFiniteDomain::new(vec![(1, 10.0), (2, 1.0), (3, 5.0)]);
let dy = CostFiniteDomain::new(vec![(1, 3.0), (2, 8.0), (3, 2.0)]);
let dz = CostFiniteDomain::new(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 = CostFiniteDomain::new(vec![(1, 10.0), (2, 1.0), (3, 5.0)]);
let dy = CostFiniteDomain::new(vec![(1, 3.0), (2, 8.0), (3, 2.0)]);
let dz = CostFiniteDomain::new(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_soft_constraints_only() {
let mut csp: Csp<FiniteDomain<i32>> = Csp::new();
let domain = FiniteDomain::new(vec![0, 1, 2]);
let x = csp.add_variable(domain.clone());
let y = csp.add_variable(domain);
csp.add_not_equal(x, y);
csp.add_soft_constraint(SoftLambdaConstraint::new(
vec![x],
move |assignment| assignment[x as usize] == Some(1),
100.0,
"prefer_x_1",
));
csp.add_soft_constraint(SoftLambdaConstraint::new(
vec![y],
move |assignment| assignment[y as usize] == Some(2),
50.0,
"prefer_y_2",
));
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
ordering: Ordering::FailFirst,
max_solutions: 1,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve(&config);
assert_eq!(solutions.len(), 1);
assert_eq!(solutions[0], vec![1, 2]);
}
#[test]
fn test_soft_constraints_unavoidable_penalty() {
let mut csp: Csp<FiniteDomain<i32>> = Csp::new();
let domain = FiniteDomain::new(vec![0, 1]);
let x = csp.add_variable(domain.clone());
let y = csp.add_variable(domain);
csp.add_not_equal(x, y);
csp.add_soft_constraint(SoftLambdaConstraint::new(
vec![x],
move |a| a[x as usize] == Some(0),
10.0,
"prefer_x_0",
));
csp.add_soft_constraint(SoftLambdaConstraint::new(
vec![y],
move |a| a[y as usize] == Some(0),
20.0,
"prefer_y_0",
));
csp.finalize();
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
ordering: Ordering::FailFirst,
max_solutions: 10,
optimization_mode: OptimizationMode::MinimizeCost,
..Default::default()
};
let solutions = csp.solve(&config);
assert!(!solutions.is_empty());
assert_eq!(solutions[0], vec![1, 0]);
}
#[test]
fn test_cost_domain_plus_soft_constraints() {
let mut csp = Csp::new();
let dx = CostFiniteDomain::new(vec![(0, 1.0), (1, 3.0)]);
let dy = CostFiniteDomain::new(vec![(0, 2.0), (1, 1.0)]);
let x = csp.add_variable(dx);
let y = csp.add_variable(dy);
csp.add_not_equal(x, y);
csp.add_soft_constraint(SoftLambdaConstraint::new(
vec![x],
move |a| a[x as usize] == Some(1),
10.0,
"prefer_x_1",
));
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());
assert_eq!(solutions[0], vec![1, 0]);
}
#[test]
fn test_feasibility_ignores_cost() {
let mut csp = Csp::new();
let domain = CostFiniteDomain::new(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 = CostFiniteDomain::new(vec![(0, 0.0), (1, 100.0), (2, 200.0)]);
let d1 = CostFiniteDomain::new(vec![(0, 0.0), (1, 100.0), (2, 200.0)]);
let d2 = CostFiniteDomain::new(vec![(0, 0.0), (1, 100.0), (2, 200.0)]);
let d3 = CostFiniteDomain::new(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 = CostFiniteDomain::new(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 = CostFiniteDomain::new(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 w in vars.windows(2) {
let (x, y) = (w[0], w[1]);
csp.add_soft_constraint(SoftLambdaConstraint::new(
vec![x, y],
move |a: &[Option<i32>]| match (&a[x as usize], &a[y as usize]) {
(Some(va), Some(vb)) => va != vb,
_ => true,
},
1.0,
"penalize_equal",
));
}
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 = CostFiniteDomain::new(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]);
}