#[cfg(test)]
#[path = "../../../tests/unit/construction/features/shared_resource_test.rs"]
mod shared_resource_test;
use super::*;
use crate::models::common::{MultiDimLoad, SingleDimLoad};
use crate::models::problem::Single;
use crate::models::solution::{Activity, Route};
use hashbrown::HashMap;
use std::cmp::Ordering;
use std::ops::{Add, Deref, RangeInclusive, Sub};
pub trait SharedResource: Add + Sub + PartialOrd + Copy + Sized + Send + Sync + Default + 'static {}
pub type SharedResourceId = usize;
pub type SharedResourceIntervalFn = Arc<dyn Fn(&RouteContext) -> Option<&Vec<(usize, usize)>> + Send + Sync>;
pub type SharedResourceCapacityFn<T> = Arc<dyn Fn(&Activity) -> Option<(T, SharedResourceId)> + Send + Sync>;
pub type SharedResourceDemandFn<T> = Arc<dyn Fn(&Single) -> Option<T> + Send + Sync>;
pub fn create_shared_resource_feature<T>(
name: &str,
total_jobs: usize,
code: ViolationCode,
resource_key: StateKey,
interval_fn: SharedResourceIntervalFn,
resource_capacity_fn: SharedResourceCapacityFn<T>,
resource_demand_fn: SharedResourceDemandFn<T>,
) -> Result<Feature, String>
where
T: SharedResource + Add<Output = T> + Sub<Output = T>,
{
FeatureBuilder::default()
.with_name(name)
.with_constraint(SharedResourceConstraint {
total_jobs,
code,
resource_key,
interval_fn: interval_fn.clone(),
resource_demand_fn: resource_demand_fn.clone(),
})
.with_state(SharedResourceState {
state_keys: vec![resource_key],
interval_fn,
resource_capacity_fn,
resource_demand_fn,
total_jobs,
resource_key,
})
.build()
}
struct SharedResourceConstraint<T: SharedResource> {
total_jobs: usize,
code: ViolationCode,
resource_key: StateKey,
interval_fn: SharedResourceIntervalFn,
resource_demand_fn: SharedResourceDemandFn<T>,
}
impl<T: SharedResource> SharedResourceConstraint<T> {
fn evaluate_route(
&self,
solution_ctx: &SolutionContext,
route_ctx: &RouteContext,
job: &Job,
) -> Option<ConstraintViolation> {
job.as_single()
.and_then(|job| {
self.resource_demand_fn.deref()(job)
.zip(self.interval_fn.deref()(route_ctx).and_then(|intervals| intervals.first()))
})
.and_then(|(_, _)| {
if solution_ctx.get_jobs_amount() != self.total_jobs {
ConstraintViolation::fail(self.code)
} else {
ConstraintViolation::success()
}
})
}
fn evaluate_activity(
&self,
route_ctx: &RouteContext,
activity_ctx: &ActivityContext,
) -> Option<ConstraintViolation> {
self.interval_fn.deref()(route_ctx)
.iter()
.flat_map(|intervals| intervals.iter())
.find(|(_, end_idx)| activity_ctx.index <= *end_idx)
.and_then(|&(start_idx, _)| {
route_ctx
.state
.get_activity_state::<T>(self.resource_key, get_activity_by_idx(&route_ctx.route, start_idx))
.and_then(|resource_available| {
let resource_demand = activity_ctx
.target
.job
.as_ref()
.and_then(|job| self.resource_demand_fn.deref()(job.as_ref()))
.unwrap_or_default();
if resource_available
.partial_cmp(&resource_demand)
.map_or(false, |ordering| ordering == Ordering::Less)
{
ConstraintViolation::skip(self.code)
} else {
ConstraintViolation::success()
}
})
})
}
}
impl<T: SharedResource> FeatureConstraint for SharedResourceConstraint<T> {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
match move_ctx {
MoveContext::Route { solution_ctx, route_ctx, job } => self.evaluate_route(solution_ctx, route_ctx, job),
MoveContext::Activity { route_ctx, activity_ctx } => self.evaluate_activity(route_ctx, activity_ctx),
}
}
fn merge(&self, source: Job, _: Job) -> Result<Job, ViolationCode> {
Ok(source)
}
}
struct SharedResourceState<T>
where
T: SharedResource + Add<Output = T> + Sub<Output = T>,
{
state_keys: Vec<StateKey>,
interval_fn: SharedResourceIntervalFn,
resource_capacity_fn: SharedResourceCapacityFn<T>,
resource_demand_fn: SharedResourceDemandFn<T>,
total_jobs: usize,
resource_key: StateKey,
}
impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> SharedResourceState<T> {
fn update_resource_consumption(&self, solution_ctx: &mut SolutionContext) {
if solution_ctx.get_jobs_amount() != self.total_jobs {
return;
}
let total_demand = solution_ctx.routes.iter().fold(HashMap::<usize, T>::default(), |acc, route_ctx| {
self.interval_fn.deref()(route_ctx).iter().flat_map(|intervals| intervals.iter()).fold(
acc,
|mut acc, &(start_idx, end_idx)| {
let activity = get_activity_by_idx(&route_ctx.route, start_idx);
let resource_demand_with_id = self.resource_capacity_fn.deref()(activity)
.map(|(_, resource_id)| (self.get_total_demand(route_ctx, start_idx..=end_idx), resource_id));
if let Some((resource_demand, id)) = resource_demand_with_id {
let entry = acc.entry(id).or_insert_with(T::default);
*entry = *entry + resource_demand;
}
acc
},
)
});
solution_ctx.routes.iter_mut().for_each(|route_ctx| {
#[allow(clippy::unnecessary_to_owned)]
self.interval_fn.deref()(route_ctx).cloned().unwrap_or_default().into_iter().for_each(|(start_idx, _)| {
let resource_available =
self.resource_capacity_fn.deref()(get_activity_by_idx(&route_ctx.route, start_idx)).and_then(
|(total_capacity, resource_id)| {
total_demand.get(&resource_id).map(|total_demand| total_capacity - *total_demand)
},
);
if let Some(resource_available) = resource_available {
let (route, state) = route_ctx.as_mut();
state.put_activity_state(
self.resource_key,
get_activity_by_idx(route, start_idx),
resource_available,
);
}
});
});
}
fn prevent_resource_consumption(&self, route_ctx: &mut RouteContext) {
self.interval_fn.deref()(route_ctx).cloned().unwrap_or_default().into_iter().for_each(
|(start_idx, end_idx)| {
let activity = get_activity_by_idx(&route_ctx.route, start_idx);
let has_resource_demand = self.resource_capacity_fn.deref()(activity).map_or(false, |(_, _)| {
(start_idx..=end_idx)
.into_iter()
.filter_map(|idx| route_ctx.route.tour.get(idx))
.filter_map(|activity| activity.job.as_ref())
.any(|job| self.resource_demand_fn.deref()(job).is_some())
});
if has_resource_demand {
let (route, state) = route_ctx.as_mut();
state.put_activity_state(self.resource_key, get_activity_by_idx(route, start_idx), T::default());
}
},
);
}
fn get_total_demand(&self, route_ctx: &RouteContext, range: RangeInclusive<usize>) -> T {
range
.into_iter()
.filter_map(|idx| route_ctx.route.tour.get(idx))
.filter_map(|activity| activity.job.as_ref())
.fold(T::default(), |acc, job| acc + self.resource_demand_fn.deref()(job).unwrap_or_default())
}
}
impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> FeatureState for SharedResourceState<T> {
fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
self.accept_route_state(solution_ctx.routes.get_mut(route_index).unwrap());
self.update_resource_consumption(solution_ctx);
}
fn accept_route_state(&self, route_ctx: &mut RouteContext) {
self.prevent_resource_consumption(route_ctx);
}
fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
self.update_resource_consumption(solution_ctx);
}
fn state_keys(&self) -> Iter<StateKey> {
self.state_keys.iter()
}
}
fn get_activity_by_idx(route: &Route, idx: usize) -> &Activity {
route.tour.get(idx).expect("cannot get activity by idx")
}
impl SharedResource for MultiDimLoad {}
impl SharedResource for SingleDimLoad {}