#[cfg(test)]
#[path = "../../../../tests/unit/solver/search/ruin/adjusted_string_removal_test.rs"]
mod adjusted_string_removal_test;
use super::Ruin;
use crate::construction::heuristics::{InsertionContext, RouteContext};
use crate::models::problem::Job;
use crate::models::solution::Tour;
use crate::solver::search::*;
use crate::solver::RefinementContext;
use crate::utils::Either;
use rosomaxa::prelude::{Float, Random};
use std::cell::RefCell;
use std::sync::Arc;
pub struct AdjustedStringRemoval {
lmax: usize,
cavg: usize,
alpha: Float,
limits: RemovalLimits,
}
impl AdjustedStringRemoval {
pub fn new(lmax: usize, cavg: usize, alpha: Float, limits: RemovalLimits) -> Self {
Self { lmax, cavg, alpha, limits }
}
pub fn new_with_defaults(limits: RemovalLimits) -> Self {
Self { lmax: 10, cavg: 10, alpha: 0.01, limits }
}
fn calculate_limits(&self, routes: &[RouteContext], random: &Arc<dyn Random>) -> (usize, usize) {
let lsmax = calculate_average_tour_cardinality(routes).min(self.lmax as Float);
let ksmax = 4. * (self.cavg as Float) / (1. + lsmax) - 1.;
let ks = random.uniform_real(1., ksmax + 1.).floor() as usize;
(lsmax as usize, ks)
}
}
impl Ruin for AdjustedStringRemoval {
fn run(&self, _: &RefinementContext, mut insertion_ctx: InsertionContext) -> InsertionContext {
let problem = insertion_ctx.problem.clone();
let random = insertion_ctx.environment.random.clone();
let tracker = RefCell::new(JobRemovalTracker::new(&self.limits, random.as_ref()));
let mut tabu_list = TabuList::from(&insertion_ctx);
let (lsmax, ks) = self.calculate_limits(insertion_ctx.solution.routes.as_slice(), &random);
let seed = select_seed_job_with_tabu_list(&insertion_ctx, &tabu_list).map(|(profile, _, job)| (profile, job));
select_neighbors(&problem, seed)
.filter(|job| !tracker.borrow().is_removed_job(job))
.take_while(|_| tracker.borrow().get_affected_actors() != ks)
.for_each(|job| {
let route_idx = insertion_ctx.solution.routes.iter().position(|route_ctx| {
!tracker.borrow().is_affected_actor(&route_ctx.route().actor)
&& route_ctx.route().tour.index(&job).is_some()
});
route_idx.into_iter().for_each(|route_idx| {
let route_ctx = insertion_ctx.solution.routes.get(route_idx).expect("invalid index");
let ltmax = route_ctx.route().tour.job_activity_count().min(lsmax);
let lt = random.uniform_real(1.0, ltmax as Float + 1.).floor() as usize;
if let Some(index) = route_ctx.route().tour.index(&job) {
select_string((&route_ctx.route().tour, index), lt, self.alpha, &random)
.collect::<Vec<Job>>()
.into_iter()
.for_each(|job| {
if tracker.borrow_mut().try_remove_job(&mut insertion_ctx.solution, route_idx, &job) {
tabu_list.add_job(job);
tabu_list.add_actor(insertion_ctx.solution.routes[route_idx].route().actor.clone());
}
});
}
});
});
tabu_list.inject(&mut insertion_ctx);
insertion_ctx
}
}
fn calculate_average_tour_cardinality(routes: &[RouteContext]) -> Float {
(routes.iter().map(|route_ctx| route_ctx.route().tour.job_activity_count() as Float).sum::<Float>()
/ (routes.len() as Float))
.round()
}
fn select_string<'a>(
seed_tour: (&'a Tour, usize),
cardinality: usize,
alpha: Float,
random: &Arc<dyn Random>,
) -> impl Iterator<Item = Job> + 'a {
if random.is_head_not_tails() {
Either::Left(sequential_string(seed_tour, cardinality, random))
} else {
Either::Right(preserved_string(seed_tour, cardinality, alpha, random))
}
}
fn sequential_string<'a>(
seed_tour: (&'a Tour, usize),
cardinality: usize,
random: &Arc<dyn Random>,
) -> impl Iterator<Item = Job> + 'a {
let (begin, end) = lower_bounds(cardinality, seed_tour.0.job_activity_count(), seed_tour.1);
let start = random.uniform_int(begin as i32, end as i32) as usize;
(start..(start + cardinality)).filter_map(move |i| seed_tour.0.get(i).and_then(|a| a.retrieve_job()))
}
fn preserved_string<'a>(
seed_tour: (&'a Tour, usize),
cardinality: usize,
alpha: Float,
random: &Arc<dyn Random>,
) -> impl Iterator<Item = Job> + 'a {
let size = seed_tour.0.job_activity_count();
let index = seed_tour.1;
let split_size = preserved_cardinality(cardinality, size, alpha, random);
let total = cardinality + split_size;
let (begin, end) = lower_bounds(total, size, index);
let start_total = random.uniform_int(begin as i32, end as i32) as usize;
let split_start = random.uniform_int(start_total as i32, (start_total + cardinality - 1) as i32) as usize;
let split_end = split_start + split_size;
let total = total - usize::from(index >= split_start && index < split_end);
(start_total..(start_total + total))
.filter(move |&i| i < split_start || i >= split_end || i == index)
.filter_map(move |i| seed_tour.0.get(i).and_then(|a| a.retrieve_job()))
}
#[allow(clippy::manual_clamp)]
fn lower_bounds(string_crd: usize, tour_crd: usize, index: usize) -> (usize, usize) {
let string_crd = string_crd as i32;
let tour_crd = tour_crd as i32;
let index = index as i32;
let start = (index - string_crd).max(1);
let end = (index + string_crd).min(tour_crd - string_crd).max(start);
(start as usize, end as usize)
}
fn preserved_cardinality(string_crd: usize, tour_crd: usize, alpha: Float, random: &Arc<dyn Random>) -> usize {
if string_crd == tour_crd {
return 0;
}
let mut preserved_crd = 1_usize;
while string_crd + preserved_crd < tour_crd {
if random.is_hit(alpha) {
break;
} else {
preserved_crd += 1
}
}
preserved_crd
}