#[cfg(test)]
#[path = "../../../../tests/unit/solver/search/local/exchange_swap_star_test.rs"]
mod exchange_swap_star_test;
use super::*;
use crate::models::common::Cost;
use crate::models::problem::Job;
use crate::solver::search::create_environment_with_custom_quota;
use crate::utils::Either;
use hashbrown::{HashMap, HashSet};
use rand::seq::SliceRandom;
use rosomaxa::utils::*;
use std::iter::once;
use std::sync::RwLock;
pub struct ExchangeSwapStar {
leg_selection: LegSelection,
result_selector: Box<dyn ResultSelector + Send + Sync>,
quota_limit: usize,
}
impl ExchangeSwapStar {
pub fn new(random: Arc<dyn Random + Send + Sync>, quota_limit: usize) -> Self {
Self {
leg_selection: LegSelection::Stochastic(random),
result_selector: Box::new(BestResultSelector::default()),
quota_limit,
}
}
}
impl LocalOperator for ExchangeSwapStar {
fn explore(
&self,
refinement_ctx: &RefinementContext,
insertion_ctx: &InsertionContext,
) -> Option<InsertionContext> {
const ROUTE_PAIRS_THRESHOLD: usize = 8;
let route_pairs = create_route_pairs(insertion_ctx, ROUTE_PAIRS_THRESHOLD);
let limit = refinement_ctx.statistics().speed.get_median().map(|median| median.max(self.quota_limit));
let mut insertion_ctx = InsertionContext {
environment: create_environment_with_custom_quota(limit, insertion_ctx.environment.as_ref()),
..insertion_ctx.deep_copy()
};
let _ = route_pairs.into_iter().try_for_each(|route_pair| {
let is_quota_reached = try_exchange_jobs_in_routes(
&mut insertion_ctx,
route_pair,
&self.leg_selection,
self.result_selector.as_ref(),
);
if is_quota_reached {
Err(())
} else {
Ok(())
}
});
Some(InsertionContext { environment: refinement_ctx.environment.clone(), ..insertion_ctx })
}
}
type SearchContext<'a> = (&'a InsertionContext, &'a LegSelection, &'a (dyn ResultSelector + Send + Sync));
fn get_route_by_idx(insertion_ctx: &InsertionContext, route_idx: usize) -> &RouteContext {
insertion_ctx.solution.routes.get(route_idx).expect("invalid route index")
}
fn get_movable_jobs(insertion_ctx: &InsertionContext, route_ctx: &RouteContext) -> Vec<Job> {
route_ctx.route.tour.jobs().filter(|job| !insertion_ctx.solution.locked.contains(job)).collect()
}
fn get_evaluation_context<'a>(search_ctx: &'a SearchContext, job: &'a Job) -> EvaluationContext<'a> {
EvaluationContext {
goal: search_ctx.0.problem.goal.as_ref(),
job,
leg_selection: search_ctx.1,
result_selector: search_ctx.2,
}
}
#[allow(clippy::needless_collect)] fn create_route_pairs(insertion_ctx: &InsertionContext, route_pairs_threshold: usize) -> Vec<(usize, usize)> {
let random = insertion_ctx.environment.random.clone();
if random.is_hit(0.1) { None } else { group_routes_by_proximity(insertion_ctx) }
.map(|route_groups_distances| {
let used_indices = RwLock::new(HashSet::<(usize, usize)>::new());
let distances = route_groups_distances
.into_iter()
.enumerate()
.flat_map(|(outer_idx, mut route_group_distance)| {
let shuffle_amount = (route_group_distance.len() as f64 * 0.1) as usize;
route_group_distance.partial_shuffle(&mut random.get_rng(), shuffle_amount);
route_group_distance
.iter()
.cloned()
.filter(|(inner_idx, _)| {
let used_indices = used_indices.read().unwrap();
!used_indices.contains(&(outer_idx, *inner_idx))
&& !used_indices.contains(&(*inner_idx, outer_idx))
})
.map(|(inner_idx, _)| {
let mut used_indices = used_indices.write().unwrap();
used_indices.insert((outer_idx, inner_idx));
used_indices.insert((inner_idx, outer_idx));
inner_idx
})
.next()
.map(|inner_idx| (outer_idx, inner_idx))
})
.collect::<Vec<_>>();
SelectionSamplingIterator::new(distances.into_iter(), route_pairs_threshold, random.clone()).collect()
})
.unwrap_or_else(|| {
let route_count = insertion_ctx.solution.routes.len();
let all_route_pairs = (0..route_count)
.flat_map(move |outer_idx| {
(0..route_count)
.filter(move |&inner_idx| outer_idx > inner_idx)
.map(move |inner_idx| (outer_idx, inner_idx))
})
.collect::<Vec<_>>();
SelectionSamplingIterator::new(all_route_pairs.into_iter(), route_pairs_threshold, random.clone()).collect()
})
}
fn find_insertion_cost(search_ctx: &SearchContext, job: &Job, route_ctx: &RouteContext) -> Cost {
let original_costs = route_ctx.get_route_cost();
let route_ctx = remove_job_with_copy(search_ctx, job, route_ctx);
original_costs - route_ctx.get_route_cost()
}
fn find_in_place_result(
search_ctx: &SearchContext,
route_ctx: &RouteContext,
insert_job: &Job,
extract_job: &Job,
) -> InsertionResult {
let insertion_index = route_ctx.route.tour.index(extract_job).expect("cannot find job in route");
let position = InsertionPosition::Concrete(insertion_index - 1);
let route_ctx = remove_job_with_copy(search_ctx, extract_job, route_ctx);
let eval_ctx = get_evaluation_context(search_ctx, insert_job);
eval_job_insertion_in_route(search_ctx.0, &eval_ctx, &route_ctx, position, InsertionResult::make_failure())
}
fn find_top_results(
search_ctx: &SearchContext,
route_ctx: &RouteContext,
jobs: &[Job],
) -> HashMap<Job, Vec<InsertionResult>> {
let legs_count = route_ctx.route.tour.legs().count();
jobs.iter()
.map(|job| {
let eval_ctx = get_evaluation_context(search_ctx, job);
let mut results = (0..legs_count)
.map(InsertionPosition::Concrete)
.map(|position| {
eval_job_insertion_in_route(
search_ctx.0,
&eval_ctx,
route_ctx,
position,
InsertionResult::make_failure(),
)
})
.collect::<Vec<_>>();
results.sort_by(|left, right| match (left, right) {
(InsertionResult::Success(_), InsertionResult::Failure(_)) => Ordering::Less,
(InsertionResult::Failure(_), InsertionResult::Success(_)) => Ordering::Greater,
(InsertionResult::Failure(_), InsertionResult::Failure(_)) => Ordering::Equal,
(InsertionResult::Success(left), InsertionResult::Success(right)) => {
compare_floats(left.cost, right.cost)
}
});
results.truncate(3);
(job.clone(), results)
})
.collect()
}
fn choose_best_result(
search_ctx: &SearchContext,
in_place_result: InsertionResult,
top_results: &[InsertionResult],
) -> InsertionResult {
let failure = InsertionResult::make_failure();
let in_place_idx = in_place_result
.as_success()
.and_then(|success| success.activities.first())
.map(|(_, idx)| *idx)
.unwrap_or(usize::MAX - 1);
let (idx, result) = once(&in_place_result)
.chain(top_results.iter().filter(|result| {
result.as_success().map_or(false, |success| {
success
.activities
.first()
.map(|(_, idx)| *idx != in_place_idx && *idx != in_place_idx + 1)
.unwrap_or(false)
})
}))
.enumerate()
.fold((0, &failure), |(acc_idx, acc_result), (idx, result)| match (acc_result, result) {
(InsertionResult::Success(acc_success), InsertionResult::Success(success)) => {
match search_ctx.2.select_cost(&acc_success.context, acc_success.cost, success.cost) {
Either::Left(_) => (acc_idx, acc_result),
Either::Right(_) => (idx, result),
}
}
(InsertionResult::Success(_), InsertionResult::Failure(_)) => (acc_idx, acc_result),
_ => (idx, result),
});
if idx == 0 {
in_place_result
} else {
match result {
InsertionResult::Success(success) => InsertionResult::Success(InsertionSuccess {
cost: success.cost,
job: success.job.clone(),
activities: success.activities.iter().map(|(activity, idx)| (activity.deep_copy(), *idx)).collect(),
context: success.context.deep_copy(),
}),
InsertionResult::Failure(_) => InsertionResult::make_failure(),
}
}
}
fn remove_job_with_copy(search_ctx: &SearchContext, job: &Job, route_ctx: &RouteContext) -> RouteContext {
let mut route_ctx = route_ctx.deep_copy();
route_ctx.route_mut().tour.remove(job);
search_ctx.0.problem.goal.accept_route_state(&mut route_ctx);
route_ctx
}
fn try_exchange_jobs_in_routes(
insertion_ctx: &mut InsertionContext,
route_pair: (usize, usize),
leg_selection: &LegSelection,
result_selector: &(dyn ResultSelector + Send + Sync),
) -> bool {
let quota = insertion_ctx.environment.quota.clone();
let is_quota_reached = move || quota.as_ref().map_or(false, |quota| quota.is_reached());
if is_quota_reached() {
return true;
}
let search_ctx: SearchContext = (insertion_ctx, leg_selection, result_selector);
let (outer_idx, inner_idx) = route_pair;
let outer_route_ctx = get_route_by_idx(insertion_ctx, outer_idx);
let inner_route_ctx = get_route_by_idx(insertion_ctx, inner_idx);
let outer_jobs = get_movable_jobs(insertion_ctx, outer_route_ctx);
let inner_jobs = get_movable_jobs(insertion_ctx, inner_route_ctx);
let outer_top_results = find_top_results(&search_ctx, inner_route_ctx, outer_jobs.as_slice());
let inner_top_results = find_top_results(&search_ctx, outer_route_ctx, inner_jobs.as_slice());
let job_pairs = outer_jobs
.iter()
.flat_map(|outer_job| {
let delta_outer_job_cost = find_insertion_cost(&search_ctx, outer_job, outer_route_ctx);
inner_jobs.iter().map(move |inner_job| (outer_job, inner_job, delta_outer_job_cost))
})
.collect::<Vec<_>>();
let (outer_best, inner_best, _) = map_reduce(
job_pairs.as_slice(),
|&(outer_job, inner_job, delta_outer_job_cost)| {
if is_quota_reached() {
return (InsertionResult::make_failure(), InsertionResult::make_failure(), Cost::default());
}
let delta_inner_job_cost = find_insertion_cost(&search_ctx, inner_job, inner_route_ctx);
let outer_in_place_result = find_in_place_result(&search_ctx, inner_route_ctx, outer_job, inner_job);
let inner_in_place_result = find_in_place_result(&search_ctx, outer_route_ctx, inner_job, outer_job);
let outer_result = choose_best_result(
&search_ctx,
outer_in_place_result,
outer_top_results.get(outer_job).unwrap().as_slice(),
);
let inner_result = choose_best_result(
&search_ctx,
inner_in_place_result,
inner_top_results.get(inner_job).unwrap().as_slice(),
);
let delta_cost = match (&outer_result, &inner_result) {
(InsertionResult::Success(outer_success), InsertionResult::Success(inner_success)) => {
outer_success.cost + inner_success.cost - delta_outer_job_cost - delta_inner_job_cost
}
_ => 0.,
};
(outer_result, inner_result, delta_cost)
},
|| (InsertionResult::make_failure(), InsertionResult::make_failure(), 0.),
|left, right| match compare_floats(left.2, right.2) {
Ordering::Less => left,
_ => right,
},
);
try_exchange_jobs(insertion_ctx, (outer_best, inner_best), leg_selection, result_selector);
is_quota_reached()
}
fn try_exchange_jobs(
insertion_ctx: &mut InsertionContext,
insertion_pair: (InsertionResult, InsertionResult),
leg_selection: &LegSelection,
result_selector: &(dyn ResultSelector + Send + Sync),
) {
if let (InsertionResult::Success(outer_success), InsertionResult::Success(inner_success)) = insertion_pair {
let constraint = insertion_ctx.problem.goal.clone();
let outer_job = outer_success.job.clone();
let inner_job = inner_success.job.clone();
let mut insertion_results = once((outer_success, inner_job))
.chain(once((inner_success, outer_job)))
.map(|(mut success, job)| {
success.context = success.context.deep_copy();
let removed_idx = success.context.route.tour.index(&job).unwrap_or(usize::MAX);
success.context.route_mut().tour.remove(&job);
constraint.accept_route_state(&mut success.context);
let position = success.activities.first().unwrap().1;
let position = if position < removed_idx || position == 0 { position } else { position - 1 };
let position = InsertionPosition::Concrete(position);
let search_ctx: SearchContext = (insertion_ctx, leg_selection, result_selector);
let eval_ctx = get_evaluation_context(&search_ctx, &success.job);
let alternative = InsertionResult::make_failure();
eval_job_insertion_in_route(insertion_ctx, &eval_ctx, &success.context, position, alternative)
})
.filter_map(|result| result.into_success())
.collect::<Vec<_>>();
if insertion_results.len() == 2 {
apply_insertion(insertion_ctx, insertion_results.pop().unwrap());
apply_insertion(insertion_ctx, insertion_results.pop().unwrap());
finalize_insertion_ctx(insertion_ctx);
}
}
}