use crate::constraint::{ConstraintEnum, VarId};
use crate::domain::Domain;
use crate::ordering::{self, Ordering};
use crate::solver::adjacency::Adjacency;
use crate::solver::optimize::DomainCostEval;
use crate::solver::{Solution, Trail, ac3, propagate};
use crate::variable::Variable;
use crate::{Pruning, SolveStats};
pub(crate) const PERMANENT_DEPTH: usize = 0;
const SEARCH_ROOT_DEPTH: usize = 1;
#[derive(Debug, Clone)]
pub(crate) struct SearchParams {
pub(crate) pruning: Pruning,
pub(crate) ordering: Ordering,
pub(crate) max_solutions: usize,
pub(crate) node_budget: Option<u64>,
pub(crate) cancel: Option<crate::CancelToken>,
}
enum Step {
Continue,
Done,
}
pub(crate) struct Kernel<'a, D: Domain> {
variables: &'a mut [Variable<D>],
constraints: &'a [ConstraintEnum<D>],
adjacency: &'a Adjacency,
weights: &'a mut [f64],
var_cids: &'a [Vec<usize>],
stats: &'a mut SolveStats,
trail: Trail,
worklist: ac3::BitsetWorklist,
params: &'a SearchParams,
}
impl<D: Domain> Kernel<'_, D>
where
D::Value: PartialEq + 'static,
{
#[inline]
fn is_valid(&self, var: VarId, assignment: &[Option<D::Value>]) -> bool {
for &ci in self.adjacency.constraints_for(var) {
let ci = ci as usize;
let scope = self.constraints[ci].scope();
if scope.iter().all(|&v| assignment[v as usize].is_some())
&& !self.constraints[ci].check(assignment)
{
return false;
}
}
true
}
#[inline]
fn propagate_from(
&mut self,
var: VarId,
assignment: &mut Vec<Option<D::Value>>,
depth: usize,
) -> bool {
match self.params.pruning {
Pruning::None => false,
Pruning::ForwardChecking => propagate::forward_check(
var,
self.variables,
self.constraints,
self.adjacency,
assignment.as_mut_slice(),
self.stats,
&mut self.trail,
depth,
)
.is_some(),
Pruning::Ac3 => ac3::ac3_from_variable(
var,
self.variables,
self.constraints,
self.adjacency,
assignment,
self.stats,
&mut self.trail,
&mut self.worklist,
depth,
)
.is_some(),
Pruning::AcFc => propagate::ac_fc(
var,
self.variables,
self.constraints,
self.adjacency,
assignment.as_mut_slice(),
self.stats,
&mut self.trail,
depth,
)
.is_some(),
}
}
}
trait SearchPolicy<D: Domain> {
fn on_leaf(&mut self, k: &mut Kernel<'_, D>, assignment: &[Option<D::Value>]) -> Step;
#[inline]
fn node_prune(&mut self, _k: &Kernel<'_, D>, _assignment: &[Option<D::Value>]) -> bool {
false
}
#[inline]
fn order_values(&self, _k: &Kernel<'_, D>, _var: VarId, _values: &mut [D::Value]) {}
}
fn search<D, P>(
k: &mut Kernel<'_, D>,
p: &mut P,
assignment: &mut Vec<Option<D::Value>>,
stack: &mut Vec<VarId>,
depth: usize,
) -> Step
where
D: Domain,
D::Value: PartialEq + 'static,
P: SearchPolicy<D>,
{
if stack.is_empty() {
return p.on_leaf(k, assignment);
}
if let Some(budget) = k.params.node_budget
&& k.stats.nodes_explored >= budget
{
k.stats.budget_exceeded = true;
return Step::Done;
}
if let Some(tok) = &k.params.cancel
&& tok.is_cancelled()
{
k.stats.cancelled = true;
return Step::Done;
}
k.stats.nodes_explored += 1;
if p.node_prune(k, assignment) {
return Step::Continue;
}
let idx =
ordering::select_variable(stack, k.variables, k.params.ordering, k.weights, k.var_cids)
.unwrap();
let var = stack.swap_remove(idx);
let mut values: Vec<_> = k.variables[var as usize].domain.iter().collect();
p.order_values(k, var, &mut values);
for val in values {
let mark = k.trail.checkpoint();
assignment[var as usize] = Some(val.clone());
k.variables[var as usize].restrict_to(&val, depth);
k.trail.push(var);
if k.is_valid(var, assignment)
&& !k.propagate_from(var, assignment, depth)
&& let Step::Done = search(k, p, assignment, stack, depth + 1)
{
return Step::Done;
}
k.stats.backtracks += 1;
assignment[var as usize] = None;
k.trail.undo_to(mark, depth, k.variables);
}
stack.push(var);
Step::Continue
}
struct Feasibility<D: Domain> {
solutions: Vec<Solution<D>>,
max_solutions: usize,
}
impl<D: Domain> SearchPolicy<D> for Feasibility<D>
where
D::Value: PartialEq + 'static,
{
#[inline]
fn on_leaf(&mut self, _k: &mut Kernel<'_, D>, assignment: &[Option<D::Value>]) -> Step {
self.solutions.push(
assignment
.iter()
.map(|v| v.as_ref().unwrap().clone())
.collect(),
);
if self.solutions.len() >= self.max_solutions {
Step::Done
} else {
Step::Continue
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn feasibility_search<D: Domain>(
variables: &mut [Variable<D>],
constraints: &[ConstraintEnum<D>],
adjacency: &Adjacency,
weights: &mut [f64],
var_cids: &[Vec<usize>],
params: &SearchParams,
stats: &mut SolveStats,
given: Option<&[(VarId, D::Value)]>,
) -> Vec<Solution<D>>
where
D::Value: PartialEq + 'static,
{
let num_vars = variables.len();
let mut assignment: Vec<Option<D::Value>> = vec![None; num_vars];
let mut stack: Vec<VarId> = if let Some(given) = given {
for (var, val) in given {
assignment[*var as usize] = Some(val.clone());
}
(0..num_vars as u32)
.filter(|v| assignment[*v as usize].is_none())
.collect()
} else {
(0..num_vars as u32).collect()
};
let mut policy = Feasibility {
solutions: Vec::new(),
max_solutions: params.max_solutions,
};
let mut kernel = Kernel {
variables,
constraints,
adjacency,
weights,
var_cids,
stats,
trail: Trail::default(),
worklist: ac3::BitsetWorklist::new(constraints.len()),
params,
};
search(
&mut kernel,
&mut policy,
&mut assignment,
&mut stack,
SEARCH_ROOT_DEPTH,
);
policy.solutions
}
struct ScoredSolution<D: Domain> {
solution: Solution<D>,
cost: f64,
}
struct BranchBound<'e, D: Domain> {
scored: Vec<ScoredSolution<D>>,
best_cost: f64,
maximize: bool,
eval: &'e dyn DomainCostEval<D>,
}
impl<D: Domain> BranchBound<'_, D>
where
D::Value: PartialEq + 'static,
{
fn assignment_cost(&self, k: &Kernel<'_, D>, assignment: &[Option<D::Value>]) -> f64 {
let mut cost = 0.0;
for (i, val) in assignment.iter().enumerate() {
if let Some(v) = val {
cost += self.eval.cost(&k.variables[i].domain, v);
}
}
cost
}
fn optimistic_bound(&self, k: &Kernel<'_, D>, assignment: &[Option<D::Value>]) -> f64 {
let mut bound = 0.0;
for (i, val) in assignment.iter().enumerate() {
match val {
Some(v) => bound += self.eval.cost(&k.variables[i].domain, v),
None if self.maximize => bound += self.eval.max_cost(&k.variables[i].domain),
None => bound += self.eval.min_cost(&k.variables[i].domain),
}
}
bound
}
}
impl<D: Domain> SearchPolicy<D> for BranchBound<'_, D>
where
D::Value: PartialEq + 'static,
{
#[inline]
fn on_leaf(&mut self, k: &mut Kernel<'_, D>, assignment: &[Option<D::Value>]) -> Step {
let cost = self.assignment_cost(k, assignment);
let effective = if self.maximize { -cost } else { cost };
if effective < self.best_cost {
self.best_cost = effective;
}
self.scored.push(ScoredSolution {
solution: assignment
.iter()
.map(|v| v.as_ref().unwrap().clone())
.collect(),
cost,
});
Step::Continue
}
#[inline]
fn node_prune(&mut self, k: &Kernel<'_, D>, assignment: &[Option<D::Value>]) -> bool {
let bound = self.optimistic_bound(k, assignment);
let effective = if self.maximize { -bound } else { bound };
effective >= self.best_cost
}
#[inline]
fn order_values(&self, k: &Kernel<'_, D>, var: VarId, values: &mut [D::Value]) {
let domain = &k.variables[var as usize].domain;
if self.maximize {
values.sort_by(|a, b| {
self.eval
.cost(domain, b)
.partial_cmp(&self.eval.cost(domain, a))
.unwrap_or(std::cmp::Ordering::Equal)
});
} else {
values.sort_by(|a, b| {
self.eval
.cost(domain, a)
.partial_cmp(&self.eval.cost(domain, b))
.unwrap_or(std::cmp::Ordering::Equal)
});
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn branch_and_bound<D: Domain>(
variables: &mut [Variable<D>],
constraints: &[ConstraintEnum<D>],
adjacency: &Adjacency,
weights: &mut [f64],
var_cids: &[Vec<usize>],
params: &SearchParams,
stats: &mut SolveStats,
maximize: bool,
cost_eval: &dyn DomainCostEval<D>,
) -> Vec<Solution<D>>
where
D::Value: PartialEq + 'static,
{
let num_vars = variables.len();
let mut assignment: Vec<Option<D::Value>> = vec![None; num_vars];
let mut stack: Vec<VarId> = (0..num_vars as u32).collect();
let mut policy = BranchBound {
scored: Vec::new(),
best_cost: f64::INFINITY,
maximize,
eval: cost_eval,
};
let mut kernel = Kernel {
variables,
constraints,
adjacency,
weights,
var_cids,
stats,
trail: Trail::default(),
worklist: ac3::BitsetWorklist::new(constraints.len()),
params,
};
search(
&mut kernel,
&mut policy,
&mut assignment,
&mut stack,
SEARCH_ROOT_DEPTH,
);
if maximize {
policy.scored.sort_by(|a, b| {
b.cost
.partial_cmp(&a.cost)
.unwrap_or(std::cmp::Ordering::Equal)
});
} else {
policy.scored.sort_by(|a, b| {
a.cost
.partial_cmp(&b.cost)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
policy.scored.truncate(params.max_solutions);
policy.scored.into_iter().map(|s| s.solution).collect()
}