use crate::timing::Timer;
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct AlnsConfig {
pub max_iterations: usize,
pub time_limit_ms: u64,
pub segment_size: usize,
pub score_best: f64,
pub score_better: f64,
pub score_accepted: f64,
pub reaction_factor: f64,
pub min_weight: f64,
pub initial_temperature: f64,
pub cooling_rate: f64,
pub final_temperature: f64,
pub seed: Option<u64>,
}
impl Default for AlnsConfig {
fn default() -> Self {
Self {
max_iterations: 10000,
time_limit_ms: 60000, segment_size: 100,
score_best: 33.0,
score_better: 9.0,
score_accepted: 3.0,
reaction_factor: 0.1,
min_weight: 0.1,
initial_temperature: 100.0,
cooling_rate: 0.9995,
final_temperature: 0.01,
seed: None,
}
}
}
impl AlnsConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_iterations(mut self, iterations: usize) -> Self {
self.max_iterations = iterations;
self
}
pub fn with_time_limit_ms(mut self, ms: u64) -> Self {
self.time_limit_ms = ms;
self
}
pub fn with_segment_size(mut self, size: usize) -> Self {
self.segment_size = size.max(1);
self
}
pub fn with_scores(mut self, best: f64, better: f64, accepted: f64) -> Self {
self.score_best = best;
self.score_better = better;
self.score_accepted = accepted;
self
}
pub fn with_reaction_factor(mut self, factor: f64) -> Self {
self.reaction_factor = factor.clamp(0.0, 1.0);
self
}
pub fn with_temperature(mut self, initial: f64, cooling_rate: f64, final_temp: f64) -> Self {
self.initial_temperature = initial.max(0.01);
self.cooling_rate = cooling_rate.clamp(0.9, 1.0);
self.final_temperature = final_temp.max(0.001);
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
}
#[derive(Debug, Clone)]
pub struct OperatorStats {
pub weight: f64,
pub times_used: usize,
pub total_score: f64,
pub segment_score: f64,
pub segment_uses: usize,
}
impl Default for OperatorStats {
fn default() -> Self {
Self {
weight: 1.0,
times_used: 0,
total_score: 0.0,
segment_score: 0.0,
segment_uses: 0,
}
}
}
impl OperatorStats {
pub fn new(initial_weight: f64) -> Self {
Self {
weight: initial_weight,
..Default::default()
}
}
pub fn record_use(&mut self, score: f64) {
self.times_used += 1;
self.total_score += score;
self.segment_score += score;
self.segment_uses += 1;
}
pub fn update_weight(&mut self, reaction_factor: f64, min_weight: f64) {
if self.segment_uses > 0 {
let segment_avg = self.segment_score / self.segment_uses as f64;
self.weight = self.weight * (1.0 - reaction_factor) + segment_avg * reaction_factor;
self.weight = self.weight.max(min_weight);
}
self.segment_score = 0.0;
self.segment_uses = 0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DestroyOperatorId {
Random,
Worst,
Related,
Shaw,
Custom(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepairOperatorId {
Greedy,
Regret,
Random,
BottomLeftFill,
Custom(usize),
}
#[derive(Debug, Clone)]
pub struct DestroyResult {
pub removed_indices: Vec<usize>,
pub operator: DestroyOperatorId,
}
#[derive(Debug, Clone)]
pub struct RepairResult {
pub placed_count: usize,
pub unplaced_count: usize,
pub operator: RepairOperatorId,
}
#[derive(Debug, Clone)]
pub struct AlnsProgress {
pub iteration: usize,
pub best_fitness: f64,
pub current_fitness: f64,
pub temperature: f64,
pub segment: usize,
pub elapsed_ms: u64,
pub acceptance_rate: f64,
pub best_destroy: DestroyOperatorId,
pub best_repair: RepairOperatorId,
}
#[derive(Debug, Clone)]
pub struct AlnsResult<S> {
pub best_solution: S,
pub best_fitness: f64,
pub iterations: usize,
pub elapsed_ms: u64,
pub improvements: usize,
pub final_temperature: f64,
pub destroy_weights: Vec<(DestroyOperatorId, f64)>,
pub repair_weights: Vec<(RepairOperatorId, f64)>,
}
pub trait AlnsSolution: Clone + Debug {
fn fitness(&self) -> f64;
fn placed_count(&self) -> usize;
fn total_count(&self) -> usize;
}
pub trait AlnsProblem {
type Solution: AlnsSolution;
fn create_initial_solution(&mut self) -> Self::Solution;
fn clone_solution(&self, solution: &Self::Solution) -> Self::Solution;
fn destroy_operators(&self) -> Vec<DestroyOperatorId>;
fn repair_operators(&self) -> Vec<RepairOperatorId>;
fn destroy(
&mut self,
solution: &mut Self::Solution,
operator: DestroyOperatorId,
degree: f64,
rng: &mut rand::rngs::StdRng,
) -> DestroyResult;
fn repair(
&mut self,
solution: &mut Self::Solution,
destroyed: &DestroyResult,
operator: RepairOperatorId,
) -> RepairResult;
fn relatedness(&self, solution: &Self::Solution, i: usize, j: usize) -> f64 {
let _ = (solution, i, j);
0.0
}
}
pub struct AlnsRunner {
config: AlnsConfig,
}
impl AlnsRunner {
pub fn new(config: AlnsConfig) -> Self {
Self { config }
}
pub fn run<P, F>(&self, problem: &mut P, mut progress_callback: F) -> AlnsResult<P::Solution>
where
P: AlnsProblem,
F: FnMut(&AlnsProgress),
{
use rand::prelude::*;
use rand::SeedableRng;
let mut rng = match self.config.seed {
Some(seed) => rand::rngs::StdRng::seed_from_u64(seed),
None => rand::rngs::StdRng::from_os_rng(),
};
let start_time = Timer::now();
let mut current = problem.create_initial_solution();
let mut best = problem.clone_solution(¤t);
let mut best_fitness = best.fitness();
let destroy_ops = problem.destroy_operators();
let repair_ops = problem.repair_operators();
let mut destroy_stats: Vec<(DestroyOperatorId, OperatorStats)> = destroy_ops
.iter()
.map(|&op| (op, OperatorStats::new(1.0)))
.collect();
let mut repair_stats: Vec<(RepairOperatorId, OperatorStats)> = repair_ops
.iter()
.map(|&op| (op, OperatorStats::new(1.0)))
.collect();
let mut temperature = self.config.initial_temperature;
let mut iteration = 0;
let mut segment = 0;
let mut improvements = 0;
let mut segment_accepts = 0;
let mut segment_total = 0;
loop {
let elapsed = start_time.elapsed();
let elapsed_ms = elapsed.as_millis() as u64;
if iteration >= self.config.max_iterations {
break;
}
if self.config.time_limit_ms > 0 && elapsed_ms >= self.config.time_limit_ms {
break;
}
let destroy_idx = self.select_operator_by_weight(&destroy_stats, &mut rng);
let destroy_op = destroy_stats[destroy_idx].0;
let repair_idx = self.select_operator_by_weight(&repair_stats, &mut rng);
let repair_op = repair_stats[repair_idx].0;
let mut candidate = problem.clone_solution(¤t);
let degree = rng.random_range(0.1..=0.4);
let destroy_result = problem.destroy(&mut candidate, destroy_op, degree, &mut rng);
let _repair_result = problem.repair(&mut candidate, &destroy_result, repair_op);
let candidate_fitness = candidate.fitness();
let current_fitness = current.fitness();
let (accepted, score) = if candidate_fitness < best_fitness {
best = problem.clone_solution(&candidate);
best_fitness = candidate_fitness;
improvements += 1;
(true, self.config.score_best)
} else if candidate_fitness < current_fitness {
(true, self.config.score_better)
} else {
let delta = candidate_fitness - current_fitness;
let accept_prob = (-delta / temperature).exp();
if rng.random::<f64>() < accept_prob {
(true, self.config.score_accepted)
} else {
(false, 0.0)
}
};
if accepted {
current = candidate;
segment_accepts += 1;
}
destroy_stats[destroy_idx].1.record_use(score);
repair_stats[repair_idx].1.record_use(score);
segment_total += 1;
temperature *= self.config.cooling_rate;
temperature = temperature.max(self.config.final_temperature);
if iteration > 0 && iteration % self.config.segment_size == 0 {
for (_, stats) in &mut destroy_stats {
stats.update_weight(self.config.reaction_factor, self.config.min_weight);
}
for (_, stats) in &mut repair_stats {
stats.update_weight(self.config.reaction_factor, self.config.min_weight);
}
segment += 1;
segment_accepts = 0;
segment_total = 0;
}
let acceptance_rate = if segment_total > 0 {
segment_accepts as f64 / segment_total as f64
} else {
0.0
};
let best_destroy = destroy_stats
.iter()
.max_by(|a, b| {
a.1.weight
.partial_cmp(&b.1.weight)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(op, _)| *op)
.unwrap_or(DestroyOperatorId::Random);
let best_repair = repair_stats
.iter()
.max_by(|a, b| {
a.1.weight
.partial_cmp(&b.1.weight)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(op, _)| *op)
.unwrap_or(RepairOperatorId::Greedy);
let progress = AlnsProgress {
iteration,
best_fitness,
current_fitness: current.fitness(),
temperature,
segment,
elapsed_ms,
acceptance_rate,
best_destroy,
best_repair,
};
progress_callback(&progress);
iteration += 1;
}
let elapsed_ms = start_time.elapsed_ms();
AlnsResult {
best_solution: best,
best_fitness,
iterations: iteration,
elapsed_ms,
improvements,
final_temperature: temperature,
destroy_weights: destroy_stats
.iter()
.map(|(op, stats)| (*op, stats.weight))
.collect(),
repair_weights: repair_stats
.iter()
.map(|(op, stats)| (*op, stats.weight))
.collect(),
}
}
fn select_operator_by_weight<T>(
&self,
stats: &[(T, OperatorStats)],
rng: &mut rand::rngs::StdRng,
) -> usize {
use rand::prelude::*;
let total_weight: f64 = stats.iter().map(|(_, s)| s.weight).sum();
if total_weight <= 0.0 || stats.is_empty() {
return 0;
}
let mut roll = rng.random::<f64>() * total_weight;
for (i, (_, stat)) in stats.iter().enumerate() {
roll -= stat.weight;
if roll <= 0.0 {
return i;
}
}
stats.len() - 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_alns_config_default() {
let config = AlnsConfig::default();
assert_eq!(config.max_iterations, 10000);
assert_eq!(config.time_limit_ms, 60000);
assert_eq!(config.segment_size, 100);
assert!((config.score_best - 33.0).abs() < 1e-9);
}
#[test]
fn test_alns_config_builder() {
let config = AlnsConfig::new()
.with_max_iterations(5000)
.with_time_limit_ms(30000)
.with_segment_size(50)
.with_scores(10.0, 5.0, 1.0)
.with_reaction_factor(0.2)
.with_temperature(50.0, 0.999, 0.001)
.with_seed(42);
assert_eq!(config.max_iterations, 5000);
assert_eq!(config.time_limit_ms, 30000);
assert_eq!(config.segment_size, 50);
assert!((config.score_best - 10.0).abs() < 1e-9);
assert!((config.score_better - 5.0).abs() < 1e-9);
assert!((config.score_accepted - 1.0).abs() < 1e-9);
assert!((config.reaction_factor - 0.2).abs() < 1e-9);
assert!((config.initial_temperature - 50.0).abs() < 1e-9);
assert!((config.cooling_rate - 0.999).abs() < 1e-9);
assert!((config.final_temperature - 0.001).abs() < 1e-9);
assert_eq!(config.seed, Some(42));
}
#[test]
fn test_operator_stats() {
let mut stats = OperatorStats::new(1.0);
stats.record_use(10.0);
stats.record_use(20.0);
assert_eq!(stats.times_used, 2);
assert!((stats.total_score - 30.0).abs() < 1e-9);
assert!((stats.segment_score - 30.0).abs() < 1e-9);
assert_eq!(stats.segment_uses, 2);
stats.update_weight(0.5, 0.1);
assert!((stats.weight - 8.0).abs() < 1e-9);
assert!((stats.segment_score - 0.0).abs() < 1e-9);
assert_eq!(stats.segment_uses, 0);
}
#[test]
fn test_destroy_operator_ids() {
let ops = [
DestroyOperatorId::Random,
DestroyOperatorId::Worst,
DestroyOperatorId::Related,
DestroyOperatorId::Shaw,
DestroyOperatorId::Custom(0),
];
assert_eq!(ops.len(), 5);
assert_eq!(DestroyOperatorId::Random, DestroyOperatorId::Random);
assert_ne!(DestroyOperatorId::Random, DestroyOperatorId::Worst);
}
#[test]
fn test_repair_operator_ids() {
let ops = [
RepairOperatorId::Greedy,
RepairOperatorId::Regret,
RepairOperatorId::Random,
RepairOperatorId::BottomLeftFill,
RepairOperatorId::Custom(0),
];
assert_eq!(ops.len(), 5);
assert_eq!(RepairOperatorId::Greedy, RepairOperatorId::Greedy);
assert_ne!(RepairOperatorId::Greedy, RepairOperatorId::Regret);
}
#[test]
fn test_destroy_result() {
let result = DestroyResult {
removed_indices: vec![0, 3, 5],
operator: DestroyOperatorId::Random,
};
assert_eq!(result.removed_indices.len(), 3);
assert_eq!(result.operator, DestroyOperatorId::Random);
}
#[test]
fn test_repair_result() {
let result = RepairResult {
placed_count: 8,
unplaced_count: 2,
operator: RepairOperatorId::Greedy,
};
assert_eq!(result.placed_count, 8);
assert_eq!(result.unplaced_count, 2);
assert_eq!(result.operator, RepairOperatorId::Greedy);
}
#[test]
fn test_alns_progress() {
let progress = AlnsProgress {
iteration: 100,
best_fitness: 0.85,
current_fitness: 0.90,
temperature: 50.0,
segment: 1,
elapsed_ms: 5000,
acceptance_rate: 0.45,
best_destroy: DestroyOperatorId::Worst,
best_repair: RepairOperatorId::Greedy,
};
assert_eq!(progress.iteration, 100);
assert!((progress.best_fitness - 0.85).abs() < 1e-9);
assert_eq!(progress.segment, 1);
assert_eq!(progress.best_destroy, DestroyOperatorId::Worst);
assert_eq!(progress.best_repair, RepairOperatorId::Greedy);
}
#[derive(Clone, Debug)]
struct MockSolution {
fitness: f64,
placed: usize,
total: usize,
}
impl AlnsSolution for MockSolution {
fn fitness(&self) -> f64 {
self.fitness
}
fn placed_count(&self) -> usize {
self.placed
}
fn total_count(&self) -> usize {
self.total
}
}
struct MockProblem {
improvement_per_iteration: f64,
}
impl AlnsProblem for MockProblem {
type Solution = MockSolution;
fn create_initial_solution(&mut self) -> MockSolution {
MockSolution {
fitness: 1.0,
placed: 8,
total: 10,
}
}
fn clone_solution(&self, solution: &MockSolution) -> MockSolution {
solution.clone()
}
fn destroy_operators(&self) -> Vec<DestroyOperatorId> {
vec![DestroyOperatorId::Random, DestroyOperatorId::Worst]
}
fn repair_operators(&self) -> Vec<RepairOperatorId> {
vec![RepairOperatorId::Greedy, RepairOperatorId::BottomLeftFill]
}
fn destroy(
&mut self,
_solution: &mut MockSolution,
operator: DestroyOperatorId,
_degree: f64,
_rng: &mut rand::rngs::StdRng,
) -> DestroyResult {
DestroyResult {
removed_indices: vec![0, 1, 2],
operator,
}
}
fn repair(
&mut self,
solution: &mut MockSolution,
_destroyed: &DestroyResult,
operator: RepairOperatorId,
) -> RepairResult {
solution.fitness -= self.improvement_per_iteration;
solution.fitness = solution.fitness.max(0.1);
RepairResult {
placed_count: solution.placed,
unplaced_count: 0,
operator,
}
}
}
#[test]
fn test_alns_runner_basic() {
let config = AlnsConfig::new()
.with_max_iterations(100)
.with_time_limit_ms(5000)
.with_seed(42);
let mut problem = MockProblem {
improvement_per_iteration: 0.01,
};
let runner = AlnsRunner::new(config);
let mut last_progress: Option<AlnsProgress> = None;
let result = runner.run(&mut problem, |progress| {
last_progress = Some(progress.clone());
});
assert!(result.best_fitness <= 1.0);
assert_eq!(result.iterations, 100);
assert!(last_progress.is_some());
assert!(!result.destroy_weights.is_empty());
assert!(!result.repair_weights.is_empty());
}
#[test]
fn test_alns_runner_time_limit() {
let config = AlnsConfig::new()
.with_max_iterations(1_000_000)
.with_time_limit_ms(100)
.with_seed(42);
let mut problem = MockProblem {
improvement_per_iteration: 0.001,
};
let runner = AlnsRunner::new(config);
let result = runner.run(&mut problem, |_| {});
assert!(result.iterations < 1_000_000);
assert!(result.elapsed_ms >= 100);
}
#[test]
fn test_alns_weight_adaptation() {
let config = AlnsConfig::new()
.with_max_iterations(200)
.with_segment_size(50)
.with_reaction_factor(0.5)
.with_seed(42);
let mut problem = MockProblem {
improvement_per_iteration: 0.01,
};
let runner = AlnsRunner::new(config);
let result = runner.run(&mut problem, |_| {});
let _initial_weight_sum: f64 = 2.0; let _final_destroy_sum: f64 = result.destroy_weights.iter().map(|(_, w)| *w).sum();
let max_destroy_weight = result
.destroy_weights
.iter()
.map(|(_, w)| *w)
.fold(0.0, f64::max);
assert!(max_destroy_weight >= 0.1); assert!(result.iterations == 200);
}
}