use crate::timing::Timer;
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct GdrrConfig {
pub max_iterations: usize,
pub time_limit_ms: u64,
pub min_ruin_ratio: f64,
pub max_ruin_ratio: f64,
pub lahc_list_length: usize,
pub initial_goal_ratio: f64,
pub goal_decrease_rate: f64,
pub min_goal_ratio: f64,
pub stagnation_threshold: usize,
pub seed: Option<u64>,
pub random_ruin_weight: f64,
pub cluster_ruin_weight: f64,
pub worst_ruin_weight: f64,
}
impl Default for GdrrConfig {
fn default() -> Self {
Self {
max_iterations: 10000,
time_limit_ms: 60000, min_ruin_ratio: 0.1,
max_ruin_ratio: 0.4,
lahc_list_length: 50,
initial_goal_ratio: 1.2,
goal_decrease_rate: 0.995,
min_goal_ratio: 0.8,
stagnation_threshold: 100,
seed: None,
random_ruin_weight: 1.0,
cluster_ruin_weight: 1.0,
worst_ruin_weight: 1.0,
}
}
}
impl GdrrConfig {
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_ruin_ratio(mut self, min: f64, max: f64) -> Self {
self.min_ruin_ratio = min.clamp(0.0, 1.0);
self.max_ruin_ratio = max.clamp(self.min_ruin_ratio, 1.0);
self
}
pub fn with_lahc_list_length(mut self, length: usize) -> Self {
self.lahc_list_length = length.max(1);
self
}
pub fn with_goal_params(
mut self,
initial_ratio: f64,
decrease_rate: f64,
min_ratio: f64,
) -> Self {
self.initial_goal_ratio = initial_ratio.max(1.0);
self.goal_decrease_rate = decrease_rate.clamp(0.9, 1.0);
self.min_goal_ratio = min_ratio.clamp(0.5, 1.0);
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_ruin_weights(mut self, random: f64, cluster: f64, worst: f64) -> Self {
self.random_ruin_weight = random.max(0.0);
self.cluster_ruin_weight = cluster.max(0.0);
self.worst_ruin_weight = worst.max(0.0);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuinType {
Random,
Cluster,
Worst,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecreateType {
BestFit,
BottomLeftFill,
NfpGuided,
}
#[derive(Debug, Clone)]
pub struct RuinedItem {
pub index: usize,
pub geometry_id: String,
pub position: Vec<f64>,
pub rotation: f64,
pub score: f64,
}
#[derive(Debug, Clone)]
pub struct RuinResult {
pub removed_items: Vec<RuinedItem>,
pub ruin_type: RuinType,
}
#[derive(Debug, Clone)]
pub struct RecreateResult {
pub placed_count: usize,
pub unplaced_count: usize,
pub recreate_type: RecreateType,
}
#[derive(Debug, Clone)]
pub struct GdrrProgress {
pub iteration: usize,
pub best_fitness: f64,
pub current_fitness: f64,
pub current_goal: f64,
pub stagnation_count: usize,
pub elapsed_ms: u64,
pub acceptance_rate: f64,
}
#[derive(Debug, Clone)]
pub struct GdrrResult<S> {
pub best_solution: S,
pub best_fitness: f64,
pub iterations: usize,
pub elapsed_ms: u64,
pub improvements: usize,
pub final_goal: f64,
}
pub trait GdrrSolution: Clone + Debug {
fn fitness(&self) -> f64;
fn placed_count(&self) -> usize;
fn total_count(&self) -> usize;
fn utilization(&self) -> f64;
fn fits_goal(&self, goal: f64) -> bool;
}
pub trait GdrrProblem {
type Solution: GdrrSolution;
fn create_initial_solution(&mut self) -> Self::Solution;
fn clone_solution(&self, solution: &Self::Solution) -> Self::Solution;
fn ruin_random(
&mut self,
solution: &mut Self::Solution,
ratio: f64,
rng: &mut rand::rngs::StdRng,
) -> RuinResult;
fn ruin_cluster(
&mut self,
solution: &mut Self::Solution,
ratio: f64,
rng: &mut rand::rngs::StdRng,
) -> RuinResult;
fn ruin_worst(
&mut self,
solution: &mut Self::Solution,
ratio: f64,
rng: &mut rand::rngs::StdRng,
) -> RuinResult;
fn recreate_best_fit(
&mut self,
solution: &mut Self::Solution,
ruined: &RuinResult,
) -> RecreateResult;
fn recreate_blf(
&mut self,
solution: &mut Self::Solution,
ruined: &RuinResult,
) -> RecreateResult;
fn recreate_nfp(
&mut self,
solution: &mut Self::Solution,
ruined: &RuinResult,
) -> RecreateResult;
fn placement_score(&self, solution: &Self::Solution, item_index: usize) -> f64;
fn get_neighbors(
&self,
solution: &Self::Solution,
item_index: usize,
radius: f64,
) -> Vec<usize>;
}
pub struct GdrrRunner {
config: GdrrConfig,
}
impl GdrrRunner {
pub fn new(config: GdrrConfig) -> Self {
Self { config }
}
pub fn run<P, F>(&self, problem: &mut P, mut progress_callback: F) -> GdrrResult<P::Solution>
where
P: GdrrProblem,
F: FnMut(&GdrrProgress),
{
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 mut lahc_list: Vec<f64> = vec![best_fitness; self.config.lahc_list_length];
let mut lahc_index = 0;
let optimal_estimate = best_fitness;
let mut current_goal = optimal_estimate * self.config.initial_goal_ratio;
let mut iteration = 0;
let mut stagnation_count = 0;
let mut improvements = 0;
let mut _accepted_count = 0;
let acceptance_window = 100;
let mut recent_accepts: Vec<bool> = Vec::with_capacity(acceptance_window);
let total_ruin_weight = self.config.random_ruin_weight
+ self.config.cluster_ruin_weight
+ self.config.worst_ruin_weight;
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 ruin_ratio =
rng.random_range(self.config.min_ruin_ratio..=self.config.max_ruin_ratio);
let ruin_roll: f64 = rng.random::<f64>() * total_ruin_weight;
let ruin_type = if ruin_roll < self.config.random_ruin_weight {
RuinType::Random
} else if ruin_roll < self.config.random_ruin_weight + self.config.cluster_ruin_weight {
RuinType::Cluster
} else {
RuinType::Worst
};
let mut candidate = problem.clone_solution(¤t);
let ruin_result = match ruin_type {
RuinType::Random => problem.ruin_random(&mut candidate, ruin_ratio, &mut rng),
RuinType::Cluster => problem.ruin_cluster(&mut candidate, ruin_ratio, &mut rng),
RuinType::Worst => problem.ruin_worst(&mut candidate, ruin_ratio, &mut rng),
};
let recreate_type = match iteration % 3 {
0 => RecreateType::BestFit,
1 => RecreateType::BottomLeftFill,
_ => RecreateType::NfpGuided,
};
let _recreate_result = match recreate_type {
RecreateType::BestFit => problem.recreate_best_fit(&mut candidate, &ruin_result),
RecreateType::BottomLeftFill => problem.recreate_blf(&mut candidate, &ruin_result),
RecreateType::NfpGuided => problem.recreate_nfp(&mut candidate, &ruin_result),
};
let candidate_fitness = candidate.fitness();
let lahc_threshold = lahc_list[lahc_index % self.config.lahc_list_length];
let current_fitness = current.fitness();
let accepted = candidate_fitness <= current_fitness
|| candidate_fitness <= lahc_threshold
|| (candidate.fits_goal(current_goal) && !current.fits_goal(current_goal));
if accepted {
current = candidate;
lahc_list[lahc_index % self.config.lahc_list_length] = current.fitness();
_accepted_count += 1;
if current.fitness() < best_fitness {
best = problem.clone_solution(¤t);
best_fitness = best.fitness();
stagnation_count = 0;
improvements += 1;
} else {
stagnation_count += 1;
}
} else {
stagnation_count += 1;
}
if recent_accepts.len() >= acceptance_window {
recent_accepts.remove(0);
}
recent_accepts.push(accepted);
if stagnation_count >= self.config.stagnation_threshold {
current_goal *= self.config.goal_decrease_rate;
current_goal = current_goal.max(optimal_estimate * self.config.min_goal_ratio);
}
let acceptance_rate = if recent_accepts.is_empty() {
0.0
} else {
recent_accepts.iter().filter(|&&a| a).count() as f64 / recent_accepts.len() as f64
};
let progress = GdrrProgress {
iteration,
best_fitness,
current_fitness: current.fitness(),
current_goal,
stagnation_count,
elapsed_ms,
acceptance_rate,
};
progress_callback(&progress);
iteration += 1;
lahc_index += 1;
}
let elapsed_ms = start_time.elapsed_ms();
GdrrResult {
best_solution: best,
best_fitness,
iterations: iteration,
elapsed_ms,
improvements,
final_goal: current_goal,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gdrr_config_default() {
let config = GdrrConfig::default();
assert_eq!(config.max_iterations, 10000);
assert_eq!(config.time_limit_ms, 60000);
assert!((config.min_ruin_ratio - 0.1).abs() < 1e-9);
assert!((config.max_ruin_ratio - 0.4).abs() < 1e-9);
assert_eq!(config.lahc_list_length, 50);
}
#[test]
fn test_gdrr_config_builder() {
let config = GdrrConfig::new()
.with_max_iterations(5000)
.with_time_limit_ms(30000)
.with_ruin_ratio(0.2, 0.5)
.with_lahc_list_length(100)
.with_goal_params(1.5, 0.99, 0.7)
.with_seed(42)
.with_ruin_weights(2.0, 1.5, 1.0);
assert_eq!(config.max_iterations, 5000);
assert_eq!(config.time_limit_ms, 30000);
assert!((config.min_ruin_ratio - 0.2).abs() < 1e-9);
assert!((config.max_ruin_ratio - 0.5).abs() < 1e-9);
assert_eq!(config.lahc_list_length, 100);
assert!((config.initial_goal_ratio - 1.5).abs() < 1e-9);
assert!((config.goal_decrease_rate - 0.99).abs() < 1e-9);
assert!((config.min_goal_ratio - 0.7).abs() < 1e-9);
assert_eq!(config.seed, Some(42));
assert!((config.random_ruin_weight - 2.0).abs() < 1e-9);
assert!((config.cluster_ruin_weight - 1.5).abs() < 1e-9);
assert!((config.worst_ruin_weight - 1.0).abs() < 1e-9);
}
#[test]
fn test_ruin_type_variants() {
let types = [RuinType::Random, RuinType::Cluster, RuinType::Worst];
assert_eq!(types.len(), 3);
assert_eq!(RuinType::Random, RuinType::Random);
assert_ne!(RuinType::Random, RuinType::Cluster);
}
#[test]
fn test_recreate_type_variants() {
let types = [
RecreateType::BestFit,
RecreateType::BottomLeftFill,
RecreateType::NfpGuided,
];
assert_eq!(types.len(), 3);
assert_eq!(RecreateType::BestFit, RecreateType::BestFit);
assert_ne!(RecreateType::BestFit, RecreateType::BottomLeftFill);
}
#[test]
fn test_ruined_item() {
let item = RuinedItem {
index: 5,
geometry_id: "rect1".to_string(),
position: vec![100.0, 200.0],
rotation: 90.0,
score: 0.75,
};
assert_eq!(item.index, 5);
assert_eq!(item.geometry_id, "rect1");
assert_eq!(item.position, vec![100.0, 200.0]);
assert!((item.rotation - 90.0).abs() < 1e-9);
assert!((item.score - 0.75).abs() < 1e-9);
}
#[test]
fn test_ruin_result() {
let result = RuinResult {
removed_items: vec![
RuinedItem {
index: 0,
geometry_id: "a".to_string(),
position: vec![0.0, 0.0],
rotation: 0.0,
score: 0.5,
},
RuinedItem {
index: 1,
geometry_id: "b".to_string(),
position: vec![10.0, 10.0],
rotation: 45.0,
score: 0.3,
},
],
ruin_type: RuinType::Random,
};
assert_eq!(result.removed_items.len(), 2);
assert_eq!(result.ruin_type, RuinType::Random);
}
#[test]
fn test_recreate_result() {
let result = RecreateResult {
placed_count: 8,
unplaced_count: 2,
recreate_type: RecreateType::BestFit,
};
assert_eq!(result.placed_count, 8);
assert_eq!(result.unplaced_count, 2);
assert_eq!(result.recreate_type, RecreateType::BestFit);
}
#[test]
fn test_gdrr_progress() {
let progress = GdrrProgress {
iteration: 100,
best_fitness: 0.85,
current_fitness: 0.90,
current_goal: 1.1,
stagnation_count: 25,
elapsed_ms: 5000,
acceptance_rate: 0.45,
};
assert_eq!(progress.iteration, 100);
assert!((progress.best_fitness - 0.85).abs() < 1e-9);
assert!((progress.current_fitness - 0.90).abs() < 1e-9);
assert!((progress.current_goal - 1.1).abs() < 1e-9);
assert_eq!(progress.stagnation_count, 25);
assert_eq!(progress.elapsed_ms, 5000);
assert!((progress.acceptance_rate - 0.45).abs() < 1e-9);
}
#[derive(Clone, Debug)]
struct MockSolution {
fitness: f64,
placed: usize,
total: usize,
}
impl GdrrSolution for MockSolution {
fn fitness(&self) -> f64 {
self.fitness
}
fn placed_count(&self) -> usize {
self.placed
}
fn total_count(&self) -> usize {
self.total
}
fn utilization(&self) -> f64 {
self.placed as f64 / self.total as f64
}
fn fits_goal(&self, goal: f64) -> bool {
self.fitness <= goal
}
}
struct MockProblem {
improvement_per_iteration: f64,
}
impl GdrrProblem 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 ruin_random(
&mut self,
_solution: &mut MockSolution,
_ratio: f64,
_rng: &mut rand::rngs::StdRng,
) -> RuinResult {
RuinResult {
removed_items: vec![],
ruin_type: RuinType::Random,
}
}
fn ruin_cluster(
&mut self,
_solution: &mut MockSolution,
_ratio: f64,
_rng: &mut rand::rngs::StdRng,
) -> RuinResult {
RuinResult {
removed_items: vec![],
ruin_type: RuinType::Cluster,
}
}
fn ruin_worst(
&mut self,
_solution: &mut MockSolution,
_ratio: f64,
_rng: &mut rand::rngs::StdRng,
) -> RuinResult {
RuinResult {
removed_items: vec![],
ruin_type: RuinType::Worst,
}
}
fn recreate_best_fit(
&mut self,
solution: &mut MockSolution,
_ruined: &RuinResult,
) -> RecreateResult {
solution.fitness -= self.improvement_per_iteration;
solution.fitness = solution.fitness.max(0.1);
RecreateResult {
placed_count: solution.placed,
unplaced_count: 0,
recreate_type: RecreateType::BestFit,
}
}
fn recreate_blf(
&mut self,
solution: &mut MockSolution,
_ruined: &RuinResult,
) -> RecreateResult {
solution.fitness -= self.improvement_per_iteration * 0.5;
solution.fitness = solution.fitness.max(0.1);
RecreateResult {
placed_count: solution.placed,
unplaced_count: 0,
recreate_type: RecreateType::BottomLeftFill,
}
}
fn recreate_nfp(
&mut self,
solution: &mut MockSolution,
_ruined: &RuinResult,
) -> RecreateResult {
solution.fitness -= self.improvement_per_iteration * 0.8;
solution.fitness = solution.fitness.max(0.1);
RecreateResult {
placed_count: solution.placed,
unplaced_count: 0,
recreate_type: RecreateType::NfpGuided,
}
}
fn placement_score(&self, _solution: &MockSolution, _item_index: usize) -> f64 {
0.5
}
fn get_neighbors(
&self,
_solution: &MockSolution,
_item_index: usize,
_radius: f64,
) -> Vec<usize> {
vec![]
}
}
#[test]
fn test_gdrr_runner_basic() {
let config = GdrrConfig::new()
.with_max_iterations(100)
.with_time_limit_ms(5000)
.with_seed(42);
let mut problem = MockProblem {
improvement_per_iteration: 0.01,
};
let runner = GdrrRunner::new(config);
let mut last_progress: Option<GdrrProgress> = 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());
}
#[test]
fn test_gdrr_runner_time_limit() {
let config = GdrrConfig::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 = GdrrRunner::new(config);
let result = runner.run(&mut problem, |_| {});
assert!(result.iterations < 1_000_000);
assert!(result.elapsed_ms >= 100);
}
}