Skip to content

Commit

Permalink
Introduce a new way of working with route state
Browse files Browse the repository at this point in the history
  • Loading branch information
reinterpretcat committed Jun 20, 2024
1 parent 1dbbc03 commit f7d7363
Show file tree
Hide file tree
Showing 39 changed files with 336 additions and 575 deletions.
1 change: 1 addition & 0 deletions vrp-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ rustc-hash.workspace = true

nohash-hasher = "0.2.0"
tinyvec = { version = "1.6.0", features = ["alloc"] }
paste = "1.0.15"
5 changes: 2 additions & 3 deletions vrp-core/src/construction/enablers/multi_trip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@ pub enum MarkerInsertionPolicy {
/// Creates a feature with multi trip functionality.
pub fn create_multi_trip_feature(
name: &str,
state_keys: Vec<StateKey>,
violation_code: ViolationCode,
policy: MarkerInsertionPolicy,
multi_trip: Arc<dyn MultiTrip + Send + Sync>,
) -> Result<Feature, GenericError> {
// NOTE guard from a mistake for not including an interval key. Should we just assert?
let state_keys = match multi_trip.get_route_intervals().get_interval_key() {
Some(key) if !state_keys.contains(&key) => state_keys.iter().copied().chain(once(key)).collect(),
_ => state_keys.to_vec(),
Some(key) => vec![key],
_ => vec![],
};

FeatureBuilder::default()
Expand Down
182 changes: 66 additions & 116 deletions vrp-core/src/construction/features/capacity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,108 +7,66 @@ mod capacity_test;
use super::*;
use crate::construction::enablers::*;
use crate::models::solution::Activity;
use std::iter::once;
use std::marker::PhantomData;
use std::sync::Arc;

/// Provides way to use capacity feature.
pub trait CapacityAspects<T: LoadOps>: Send + Sync {
/// Gets vehicle's capacity.
fn get_capacity<'a>(&self, vehicle: &'a Vehicle) -> Option<&'a T>;
custom_activity_state!(CurrentCapacity typeof T: LoadOps);

/// Gets job's demand.
fn get_demand<'a>(&self, single: &'a Single) -> Option<&'a Demand<T>>;
custom_activity_state!(MaxFutureCapacity typeof T: LoadOps);

/// Sets job's new demand.
fn set_demand(&self, single: &mut Single, demand: Demand<T>);
custom_activity_state!(MaxPastCapacity typeof T: LoadOps);

/// Gets capacity state keys.
fn get_state_keys(&self) -> &CapacityKeys;
custom_tour_state!(MaxVehicleLoad typeof f64);

/// Gets violation code.
fn get_violation_code(&self) -> ViolationCode;
}
custom_dimension!(VehicleCapacity typeof T: LoadOps);

/// Combines all state keys needed for capacity feature usage.
#[derive(Clone, Debug)]
pub struct CapacityKeys {
/// A key which tracks current vehicle capacity.
pub current_capacity: StateKey,
/// A key which tracks maximum vehicle capacity ahead in route.
pub max_future_capacity: StateKey,
/// A key which tracks maximum capacity backward in route.
pub max_past_capacity: StateKey,
/// A key which tracks max load in tour.
pub max_load: StateKey,
}
/// A trait to get or set job demand.
pub trait JobDemandDimension {
/// Sets job demand.
fn set_job_demand<T: LoadOps>(&mut self, demand: Demand<T>) -> &mut Self;

impl From<&mut StateKeyRegistry> for CapacityKeys {
fn from(state_registry: &mut StateKeyRegistry) -> Self {
Self {
current_capacity: state_registry.next_key(),
max_future_capacity: state_registry.next_key(),
max_past_capacity: state_registry.next_key(),
max_load: state_registry.next_key(),
}
}
}

impl CapacityKeys {
fn iter(&self) -> impl Iterator<Item = StateKey> {
once(self.current_capacity)
.chain(once(self.max_future_capacity))
.chain(once(self.max_past_capacity))
.chain(once(self.max_load))
}
/// Gets job demand.
fn get_job_demand<T: LoadOps>(&self) -> Option<&Demand<T>>;
}

/// Creates capacity feature as a hard constraint with multi trip functionality as a soft constraint.
pub fn create_capacity_limit_with_multi_trip_feature<T, A>(
pub fn create_capacity_limit_with_multi_trip_feature<T>(
name: &str,
route_intervals: RouteIntervals,
aspects: A,
violation_code: ViolationCode,
) -> Result<Feature, GenericError>
where
T: LoadOps,
A: CapacityAspects<T> + 'static,
{
let feature_keys = aspects.get_state_keys().iter().collect::<Vec<_>>();
let capacity_code = aspects.get_violation_code();
create_multi_trip_feature(
name,
feature_keys.clone(),
capacity_code,
violation_code,
MarkerInsertionPolicy::Last,
Arc::new(CapacitatedMultiTrip::<T, A> { route_intervals, aspects, phantom: Default::default() }),
Arc::new(CapacitatedMultiTrip::<T> { route_intervals, violation_code, phantom: Default::default() }),
)
}

/// Creates capacity feature as a hard constraint.
pub fn create_capacity_limit_feature<T, A>(name: &str, aspects: A) -> Result<Feature, GenericError>
pub fn create_capacity_limit_feature<T>(name: &str, violation_code: ViolationCode) -> Result<Feature, GenericError>
where
T: LoadOps,
A: CapacityAspects<T> + 'static,
{
let feature_keys = aspects.get_state_keys().iter().collect::<Vec<_>>();
let capacity_code = aspects.get_violation_code();
// TODO theoretically, the code can be easily refactored to get opt-out from no-op multi-trip runtime overhead here
create_multi_trip_feature(
name,
feature_keys,
capacity_code,
violation_code,
MarkerInsertionPolicy::Last,
Arc::new(CapacitatedMultiTrip::<T, A> {
Arc::new(CapacitatedMultiTrip::<T> {
route_intervals: RouteIntervals::Single,
aspects,
violation_code,
phantom: Default::default(),
}),
)
}

impl<T, A> FeatureConstraint for CapacitatedMultiTrip<T, A>
impl<T> FeatureConstraint for CapacitatedMultiTrip<T>
where
T: LoadOps,
A: CapacityAspects<T> + 'static,
{
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
match move_ctx {
Expand All @@ -120,8 +78,8 @@ where
fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
match (&source, &candidate) {
(Job::Single(s_source), Job::Single(s_candidate)) => {
let source_demand: Option<&Demand<T>> = self.aspects.get_demand(s_source);
let candidate_demand: Option<&Demand<T>> = self.aspects.get_demand(s_candidate);
let source_demand: Option<&Demand<T>> = s_source.dimens.get_job_demand();
let candidate_demand: Option<&Demand<T>> = s_candidate.dimens.get_job_demand();

match (source_demand, candidate_demand) {
(None, None) | (Some(_), None) => Ok(source),
Expand All @@ -131,31 +89,29 @@ where
let new_demand = source_demand + candidate_demand;

let mut single = Single { places: s_source.places.clone(), dimens: s_source.dimens.clone() };
self.aspects.set_demand(&mut single, new_demand);
single.dimens.set_job_demand(new_demand);

Ok(Job::Single(Arc::new(single)))
}
}
}
_ => Err(self.aspects.get_violation_code()),
_ => Err(self.violation_code),
}
}
}

struct CapacitatedMultiTrip<T, A>
struct CapacitatedMultiTrip<T>
where
T: LoadOps,
A: CapacityAspects<T> + 'static,
{
route_intervals: RouteIntervals,
aspects: A,
violation_code: ViolationCode,
phantom: PhantomData<T>,
}

impl<T, A> MultiTrip for CapacitatedMultiTrip<T, A>
impl<T> MultiTrip for CapacitatedMultiTrip<T>
where
T: LoadOps,
A: CapacityAspects<T> + 'static,
{
fn get_route_intervals(&self) -> &RouteIntervals {
&self.route_intervals
Expand All @@ -166,8 +122,6 @@ where
}

fn recalculate_states(&self, route_ctx: &mut RouteContext) {
let state_keys = self.aspects.get_state_keys();

let marker_intervals = self
.get_route_intervals()
.get_marker_intervals(route_ctx)
Expand Down Expand Up @@ -221,12 +175,12 @@ where
(current - end_pickup, current_max.max_load(max))
});

route_ctx.state_mut().put_activity_states(state_keys.current_capacity, current_capacities);
route_ctx.state_mut().put_activity_states(state_keys.max_past_capacity, max_past_capacities);
route_ctx.state_mut().put_activity_states(state_keys.max_future_capacity, max_future_capacities);
route_ctx.state_mut().set_current_capacity_states(current_capacities);
route_ctx.state_mut().set_max_past_capacity_states(max_past_capacities);
route_ctx.state_mut().set_max_future_capacity_states(max_future_capacities);

if let Some(capacity) = self.aspects.get_capacity(&route_ctx.route().actor.clone().vehicle) {
route_ctx.state_mut().put_route_state(state_keys.max_load, max_load.ratio(capacity));
if let Some(capacity) = route_ctx.route().actor.clone().vehicle.dimens.get_vehicle_capacity::<T>() {
route_ctx.state_mut().set_max_vehicle_load(max_load.ratio(capacity));
}
}

Expand All @@ -236,24 +190,23 @@ where
}
}

impl<T, A> CapacitatedMultiTrip<T, A>
impl<T> CapacitatedMultiTrip<T>
where
T: LoadOps,
A: CapacityAspects<T> + 'static,
{
fn evaluate_job(&self, route_ctx: &RouteContext, job: &Job) -> Option<ConstraintViolation> {
let can_handle = match job {
Job::Single(job) => self.can_handle_demand_on_intervals(route_ctx, self.aspects.get_demand(job), None),
Job::Single(job) => self.can_handle_demand_on_intervals(route_ctx, job.dimens.get_job_demand(), None),
Job::Multi(job) => job
.jobs
.iter()
.any(|job| self.can_handle_demand_on_intervals(route_ctx, self.aspects.get_demand(job), None)),
.any(|job| self.can_handle_demand_on_intervals(route_ctx, job.dimens.get_job_demand(), None)),
};

if can_handle {
ConstraintViolation::success()
} else {
ConstraintViolation::fail(self.aspects.get_violation_code())
ConstraintViolation::fail(self.violation_code)
}
}

Expand All @@ -272,17 +225,10 @@ where
Some(false)
}
} else {
has_demand_violation(
route_ctx.state(),
activity_ctx.index,
self.aspects.get_capacity(&route_ctx.route().actor.vehicle),
demand,
self.aspects.get_state_keys(),
!self.has_markers(route_ctx),
)
has_demand_violation(route_ctx, activity_ctx.index, demand, !self.has_markers(route_ctx))
};

violation.map(|stopped| ConstraintViolation { code: self.aspects.get_violation_code(), stopped })
violation.map(|stopped| ConstraintViolation { code: self.violation_code, stopped })
}

fn has_markers(&self, route_ctx: &RouteContext) -> bool {
Expand All @@ -295,16 +241,7 @@ where
demand: Option<&Demand<T>>,
insert_idx: Option<usize>,
) -> bool {
let has_demand_violation = |activity_idx: usize| {
has_demand_violation(
route_ctx.state(),
activity_idx,
self.aspects.get_capacity(&route_ctx.route().actor.vehicle),
demand,
self.aspects.get_state_keys(),
true,
)
};
let has_demand_violation = |activity_idx: usize| has_demand_violation(route_ctx, activity_idx, demand, true);

let has_demand_violation_on_borders = |start_idx: usize, end_idx: usize| {
has_demand_violation(start_idx).is_none() || has_demand_violation(end_idx).is_none()
Expand All @@ -326,43 +263,45 @@ where
if let Some(insert_idx) = insert_idx {
has_demand_violation(insert_idx).is_none()
} else {
has_demand_violation_on_borders(0, route_ctx.route().tour.total().max(1) - 1)
let last_idx = route_ctx.route().tour.end_idx().unwrap_or_default();
has_demand_violation_on_borders(0, last_idx)
}
})
}

fn get_demand<'a>(&self, activity: &'a Activity) -> Option<&'a Demand<T>> {
activity.job.as_ref().and_then(|single| self.aspects.get_demand(single))
activity.job.as_ref().and_then(|single| single.dimens.get_job_demand())
}
}

fn has_demand_violation<T: LoadOps>(
state: &RouteState,
route_ctx: &RouteContext,
pivot_idx: usize,
capacity: Option<&T>,
demand: Option<&Demand<T>>,
feature_keys: &CapacityKeys,
stopped: bool,
) -> Option<bool> {
let capacity: Option<&T> = route_ctx.route().actor.vehicle.dimens.get_vehicle_capacity();
let demand = demand?;
let capacity = if let Some(capacity) = capacity.copied() {

let capacity = if let Some(capacity) = capacity {
capacity
} else {
return Some(stopped);
};

// check how static delivery affect past max load
let state = route_ctx.state();

// check how static delivery affects a past max load
if demand.delivery.0.is_not_empty() {
let past: T = state.get_activity_state(feature_keys.max_past_capacity, pivot_idx).copied().unwrap_or_default();
let past: T = state.get_max_past_capacity_at(pivot_idx).copied().unwrap_or_default();
if !capacity.can_fit(&(past + demand.delivery.0)) {
return Some(stopped);
}
}

// check how static pickup affect future max load
if demand.pickup.0.is_not_empty() {
let future: T =
state.get_activity_state(feature_keys.max_future_capacity, pivot_idx).copied().unwrap_or_default();
let future: T = state.get_max_future_capacity_at(pivot_idx).copied().unwrap_or_default();
if !capacity.can_fit(&(future + demand.pickup.0)) {
return Some(false);
}
Expand All @@ -371,18 +310,29 @@ fn has_demand_violation<T: LoadOps>(
// check dynamic load change
let change = demand.change();
if change.is_not_empty() {
let future: T =
state.get_activity_state(feature_keys.max_future_capacity, pivot_idx).copied().unwrap_or_default();
let future: T = state.get_max_future_capacity_at(pivot_idx).copied().unwrap_or_default();
if !capacity.can_fit(&(future + change)) {
return Some(false);
}

let current: T =
state.get_activity_state(feature_keys.current_capacity, pivot_idx).copied().unwrap_or_default();
let current: T = state.get_current_capacity_at(pivot_idx).copied().unwrap_or_default();
if !capacity.can_fit(&(current + change)) {
return Some(false);
}
}

None
}

// TODO extend macro to support this.
struct JobDemandDimenKey;
impl JobDemandDimension for Dimensions {
fn set_job_demand<T: LoadOps>(&mut self, demand: Demand<T>) -> &mut Self {
self.set_value::<JobDemandDimenKey, _>(demand);
self
}

fn get_job_demand<T: LoadOps>(&self) -> Option<&Demand<T>> {
self.get_value::<JobDemandDimenKey, _>()
}
}
3 changes: 1 addition & 2 deletions vrp-core/src/construction/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ use std::sync::Arc;
mod breaks;
pub use self::breaks::*;

mod capacity;
pub use self::capacity::*;
pub mod capacity;

mod compatibility;
pub use self::compatibility::*;
Expand Down
Loading

0 comments on commit f7d7363

Please sign in to comment.