use std::collections::{HashSet, VecDeque};
use std::hash::Hash;
use crate::core::candidate::Candidate;
use crate::core::objective::Direction;
use crate::core::population::Population;
use crate::core::problem::Problem;
use crate::core::result::OptimizationResult;
use crate::core::rng::{Rng, rng_from_seed};
use crate::traits::{Initializer, Optimizer};
#[derive(Debug, Clone)]
pub struct TabuSearchConfig {
pub iterations: usize,
pub tabu_tenure: usize,
pub seed: u64,
}
impl Default for TabuSearchConfig {
fn default() -> Self {
Self {
iterations: 500,
tabu_tenure: 16,
seed: 42,
}
}
}
pub struct TabuSearch<D, I, N>
where
D: Clone + Hash + Eq,
I: Initializer<D>,
N: FnMut(&D, &mut Rng) -> Vec<D>,
{
pub config: TabuSearchConfig,
pub initializer: I,
pub neighbors: N,
_marker: std::marker::PhantomData<D>,
}
impl<D, I, N> TabuSearch<D, I, N>
where
D: Clone + Hash + Eq,
I: Initializer<D>,
N: FnMut(&D, &mut Rng) -> Vec<D>,
{
pub fn new(config: TabuSearchConfig, initializer: I, neighbors: N) -> Self {
Self {
config,
initializer,
neighbors,
_marker: std::marker::PhantomData,
}
}
}
impl<P, I, N> Optimizer<P> for TabuSearch<P::Decision, I, N>
where
P: Problem + Sync,
P::Decision: Clone + Hash + Eq + Send,
I: Initializer<P::Decision>,
N: FnMut(&P::Decision, &mut Rng) -> Vec<P::Decision>,
{
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
let objectives = problem.objectives();
assert!(
objectives.is_single_objective(),
"TabuSearch requires exactly one objective",
);
assert!(
self.config.tabu_tenure >= 1,
"TabuSearch tabu_tenure must be >= 1",
);
let direction = objectives.objectives[0].direction;
let mut rng = rng_from_seed(self.config.seed);
let mut initial = self.initializer.initialize(1, &mut rng);
assert!(
!initial.is_empty(),
"TabuSearch initializer returned no decisions"
);
let mut current_decision = initial.remove(0);
let mut current_eval = problem.evaluate(¤t_decision);
let mut best_decision = current_decision.clone();
let mut best_eval = current_eval.clone();
let mut evaluations = 1usize;
let mut tabu_queue: VecDeque<P::Decision> =
VecDeque::with_capacity(self.config.tabu_tenure);
let mut tabu_set: HashSet<P::Decision> = HashSet::new();
for _ in 0..self.config.iterations {
let candidates = (self.neighbors)(¤t_decision, &mut rng);
if candidates.is_empty() {
break;
}
let mut best_idx: Option<usize> = None;
let mut best_cand_eval: Option<crate::core::evaluation::Evaluation> = None;
let evaluations_before = evaluations;
let mut cand_evals: Vec<crate::core::evaluation::Evaluation> =
Vec::with_capacity(candidates.len());
for c in &candidates {
cand_evals.push(problem.evaluate(c));
}
evaluations += candidates.len();
let _ = evaluations_before;
for (i, c) in candidates.iter().enumerate() {
let is_tabu = tabu_set.contains(c);
let aspires = is_tabu && better_than(&cand_evals[i], &best_eval, direction);
if is_tabu && !aspires {
continue;
}
let eligible = match &best_cand_eval {
None => true,
Some(b) => better_than(&cand_evals[i], b, direction),
};
if eligible {
best_idx = Some(i);
best_cand_eval = Some(cand_evals[i].clone());
}
}
if best_idx.is_none() {
for (i, _) in candidates.iter().enumerate() {
let eligible = match &best_cand_eval {
None => true,
Some(b) => better_than(&cand_evals[i], b, direction),
};
if eligible {
best_idx = Some(i);
best_cand_eval = Some(cand_evals[i].clone());
}
}
}
let chosen_idx = best_idx.expect("non-empty candidate list");
let chosen_decision = candidates[chosen_idx].clone();
current_eval = cand_evals.remove(chosen_idx);
current_decision = chosen_decision.clone();
if better_than(¤t_eval, &best_eval, direction) {
best_decision = current_decision.clone();
best_eval = current_eval.clone();
}
tabu_queue.push_back(chosen_decision.clone());
tabu_set.insert(chosen_decision);
if tabu_queue.len() > self.config.tabu_tenure {
if let Some(old) = tabu_queue.pop_front() {
tabu_set.remove(&old);
}
}
}
let best = Candidate::new(best_decision, best_eval);
let population = Population::new(vec![best.clone()]);
let front = vec![best.clone()];
OptimizationResult::new(
population,
front,
Some(best),
evaluations,
self.config.iterations,
)
}
}
fn better_than(
a: &crate::core::evaluation::Evaluation,
b: &crate::core::evaluation::Evaluation,
direction: Direction,
) -> bool {
match (a.is_feasible(), b.is_feasible()) {
(true, false) => true,
(false, true) => false,
(false, false) => a.constraint_violation < b.constraint_violation,
(true, true) => match direction {
Direction::Minimize => a.objectives[0] < b.objectives[0],
Direction::Maximize => a.objectives[0] > b.objectives[0],
},
}
}
#[cfg(feature = "async")]
impl<D, I, N> TabuSearch<D, I, N>
where
D: Clone + Hash + Eq,
I: Initializer<D>,
N: FnMut(&D, &mut Rng) -> Vec<D>,
{
pub async fn run_async<P>(&mut self, problem: &P, concurrency: usize) -> OptimizationResult<D>
where
P: crate::core::async_problem::AsyncProblem<Decision = D>,
D: Send + Sync,
{
use crate::algorithms::parallel_eval_async::evaluate_batch_async;
let objectives = problem.objectives();
assert!(
objectives.is_single_objective(),
"TabuSearch requires exactly one objective",
);
assert!(
self.config.tabu_tenure >= 1,
"TabuSearch tabu_tenure must be >= 1",
);
let direction = objectives.objectives[0].direction;
let mut rng = rng_from_seed(self.config.seed);
let mut initial = self.initializer.initialize(1, &mut rng);
assert!(
!initial.is_empty(),
"TabuSearch initializer returned no decisions"
);
let mut current_decision = initial.remove(0);
let mut current_eval = problem.evaluate_async(¤t_decision).await;
let mut best_decision = current_decision.clone();
let mut best_eval = current_eval.clone();
let mut evaluations = 1usize;
let mut tabu_queue: VecDeque<D> = VecDeque::with_capacity(self.config.tabu_tenure);
let mut tabu_set: HashSet<D> = HashSet::new();
for _ in 0..self.config.iterations {
let candidates = (self.neighbors)(¤t_decision, &mut rng);
if candidates.is_empty() {
break;
}
let cand_results = evaluate_batch_async(problem, candidates.clone(), concurrency).await;
let mut cand_evals: Vec<crate::core::evaluation::Evaluation> =
cand_results.into_iter().map(|c| c.evaluation).collect();
evaluations += candidates.len();
let mut best_idx: Option<usize> = None;
let mut best_cand_eval: Option<crate::core::evaluation::Evaluation> = None;
for (i, c) in candidates.iter().enumerate() {
let is_tabu = tabu_set.contains(c);
let aspires = is_tabu && better_than(&cand_evals[i], &best_eval, direction);
if is_tabu && !aspires {
continue;
}
let eligible = match &best_cand_eval {
None => true,
Some(b) => better_than(&cand_evals[i], b, direction),
};
if eligible {
best_idx = Some(i);
best_cand_eval = Some(cand_evals[i].clone());
}
}
if best_idx.is_none() {
for (i, _) in candidates.iter().enumerate() {
let eligible = match &best_cand_eval {
None => true,
Some(b) => better_than(&cand_evals[i], b, direction),
};
if eligible {
best_idx = Some(i);
best_cand_eval = Some(cand_evals[i].clone());
}
}
}
let chosen_idx = best_idx.expect("non-empty candidate list");
let chosen_decision = candidates[chosen_idx].clone();
current_eval = cand_evals.remove(chosen_idx);
current_decision = chosen_decision.clone();
if better_than(¤t_eval, &best_eval, direction) {
best_decision = current_decision.clone();
best_eval = current_eval.clone();
}
tabu_queue.push_back(chosen_decision.clone());
tabu_set.insert(chosen_decision);
if tabu_queue.len() > self.config.tabu_tenure {
if let Some(old) = tabu_queue.pop_front() {
tabu_set.remove(&old);
}
}
}
let best = Candidate::new(best_decision, best_eval);
let population = Population::new(vec![best.clone()]);
let front = vec![best.clone()];
OptimizationResult::new(
population,
front,
Some(best),
evaluations,
self.config.iterations,
)
}
}
impl<D, I, N> crate::traits::AlgorithmInfo for TabuSearch<D, I, N>
where
D: Clone + Hash + Eq,
I: Initializer<D>,
N: FnMut(&D, &mut Rng) -> Vec<D>,
{
fn name(&self) -> &'static str {
"Tabu Search"
}
fn seed(&self) -> Option<u64> {
Some(self.config.seed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::evaluation::Evaluation;
use crate::core::objective::{Objective, ObjectiveSpace};
use rand::Rng as _;
struct GridProblem;
impl Problem for GridProblem {
type Decision = Vec<i32>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate(&self, x: &Vec<i32>) -> Evaluation {
let v = (x[0] - 7) as f64;
Evaluation::new(vec![v * v])
}
}
struct StartAtZero;
impl Initializer<Vec<i32>> for StartAtZero {
fn initialize(&mut self, size: usize, _rng: &mut Rng) -> Vec<Vec<i32>> {
(0..size).map(|_| vec![0]).collect()
}
}
fn make_optimizer<F>(seed: u64, neighbors: F) -> TabuSearch<Vec<i32>, StartAtZero, F>
where
F: FnMut(&Vec<i32>, &mut Rng) -> Vec<Vec<i32>>,
{
TabuSearch::new(
TabuSearchConfig {
iterations: 50,
tabu_tenure: 4,
seed,
},
StartAtZero,
neighbors,
)
}
#[test]
fn finds_optimum_on_grid() {
let neighbors = |x: &Vec<i32>, _rng: &mut Rng| vec![vec![x[0] - 1], vec![x[0] + 1]];
let mut opt = make_optimizer(1, neighbors);
let r = opt.run(&GridProblem);
let best = r.best.unwrap();
assert_eq!(best.decision, vec![7]);
assert_eq!(best.evaluation.objectives, vec![0.0]);
}
#[test]
fn deterministic_with_same_seed() {
let neighbors = |x: &Vec<i32>, rng: &mut Rng| {
(0..5)
.map(|_| vec![x[0] + rng.random_range(-3..=3)])
.collect::<Vec<_>>()
};
let mut a = make_optimizer(99, neighbors);
let mut b = make_optimizer(99, |x: &Vec<i32>, rng: &mut Rng| {
(0..5)
.map(|_| vec![x[0] + rng.random_range(-3..=3)])
.collect::<Vec<_>>()
});
let ra = a.run(&GridProblem);
let rb = b.run(&GridProblem);
assert_eq!(
ra.best.unwrap().evaluation.objectives,
rb.best.unwrap().evaluation.objectives,
);
}
#[test]
fn better_than_feasibility_first_and_direction() {
use crate::core::objective::Direction;
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert!(better_than(&feasible, &infeasible, Direction::Minimize));
assert!(!better_than(&infeasible, &feasible, Direction::Minimize));
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(better_than(&lo, &hi, Direction::Minimize));
assert!(better_than(&hi, &lo, Direction::Maximize));
let eq = Evaluation::new(vec![1.0]);
assert!(!better_than(&lo, &eq, Direction::Minimize));
let v_lo = Evaluation::constrained(vec![0.0], 0.2);
let v_hi = Evaluation::constrained(vec![0.0], 0.8);
assert!(better_than(&v_lo, &v_hi, Direction::Minimize));
}
}