Skip to content

Commit

Permalink
Do not use hashmaps to store activity states
Browse files Browse the repository at this point in the history
  • Loading branch information
reinterpretcat committed Dec 22, 2023
1 parent e3007d4 commit 4d545d0
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 126 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

This release combines many changes, but essentials are:
- internal route state api simplification
- increased performance
- several experimental features
- bug fixes

### Added

* original job place index in activity place to simplify activity-job place matching
Expand All @@ -24,6 +30,7 @@ All notable changes to this project will be documented in this file.
* update dependencies
* improve a bit documentation
* refactor route state
* do not use hashmaps to store activity states

### Fixed

Expand Down
41 changes: 26 additions & 15 deletions vrp-core/src/construction/enablers/schedule_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ pub struct ScheduleKeys {
pub latest_arrival: StateKey,
/// Waiting time state key.
pub waiting_time: StateKey,
/// Total distance state key.
/// Total route distance state key.
pub total_distance: StateKey,
/// Total duration state key.
/// Total route duration state key.
pub total_duration: StateKey,
/// Limit duration state key.
pub limit_duration: StateKey,
Expand Down Expand Up @@ -101,10 +101,14 @@ fn update_states(
0_f64,
);

let (route, state) = route_ctx.as_mut();
let route = route_ctx.route();
let mut latest_arrivals = Vec::with_capacity(route.tour.total());
let mut waiting_times = Vec::with_capacity(route.tour.total());

route.tour.all_activities().enumerate().rev().fold(init, |acc, (activity_idx, act)| {
route.tour.all_activities().rev().fold(init, |acc, act| {
if act.job.is_none() {
latest_arrivals.push(Default::default());
waiting_times.push(Default::default());
return acc;
}

Expand All @@ -118,11 +122,23 @@ fn update_states(
};
let future_waiting = waiting + (act.place.time.start - act.schedule.arrival).max(0.);

state.put_activity_state(state_keys.latest_arrival, activity_idx, latest_arrival_time);
state.put_activity_state(state_keys.waiting_time, activity_idx, future_waiting);
latest_arrivals.push(latest_arrival_time);
waiting_times.push(future_waiting);

(latest_arrival_time, act.place.location, future_waiting)
});

latest_arrivals.reverse();
waiting_times.reverse();

// NOTE: pop out state for arrival
if route.tour.end().map_or(false, |end| end.job.is_none()) {
latest_arrivals.pop();
waiting_times.pop();
}

route_ctx.state_mut().put_activity_states(state_keys.latest_arrival, latest_arrivals);
route_ctx.state_mut().put_activity_states(state_keys.waiting_time, waiting_times);
}

fn update_statistics(
Expand All @@ -137,16 +153,11 @@ fn update_statistics(
let total_dur = end.schedule.departure - start.schedule.departure;

let init = (start.place.location, start.schedule.departure, Distance::default());
let (_, _, total_dist) =
route.tour.all_activities().enumerate().skip(1).fold(init, |(loc, dep, total_dist), (a_idx, a)| {
let total_dist = total_dist + transport.distance(route, loc, a.place.location, TravelTime::Departure(dep));
let total_dur = a.schedule.departure - start.schedule.departure;

state.put_activity_state(state_keys.total_distance, a_idx, total_dist);
state.put_activity_state(state_keys.total_duration, a_idx, total_dur);
let (_, _, total_dist) = route.tour.all_activities().skip(1).fold(init, |(loc, dep, total_dist), a| {
let total_dist = total_dist + transport.distance(route, loc, a.place.location, TravelTime::Departure(dep));

(a.place.location, a.schedule.departure, total_dist)
});
(a.place.location, a.schedule.departure, total_dist)
});

state.put_route_state(state_keys.total_distance, total_dist);
state.put_route_state(state_keys.total_duration, total_dur);
Expand Down
22 changes: 16 additions & 6 deletions vrp-core/src/construction/features/capacity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,15 @@ impl<T: LoadOps> MultiTrip for CapacitatedMultiTrip<T> {
.cloned()
.unwrap_or_else(|| vec![(0, route_ctx.route().tour.total() - 1)]);

let tour_len = route_ctx.route().tour.total();

let mut current_capacities = vec![T::default(); tour_len];
let mut max_past_capacities = vec![T::default(); tour_len];
let mut max_future_capacities = vec![T::default(); tour_len];

let (_, max_load) =
marker_intervals.into_iter().fold((T::default(), T::default()), |(acc, max), (start_idx, end_idx)| {
let (route, state) = route_ctx.as_mut();
let route = route_ctx.route();

// determine static deliveries loaded at the begin and static pickups brought to the end
let (start_delivery, end_pickup) = route.tour.activities_slice(start_idx, end_idx).iter().fold(
Expand All @@ -171,23 +177,27 @@ impl<T: LoadOps> MultiTrip for CapacitatedMultiTrip<T> {
let current = current + change;
let max = max.max_load(current);

state.put_activity_state(self.feature_keys.current_capacity, activity_idx, current);
state.put_activity_state(self.feature_keys.max_past_capacity, activity_idx, max);
current_capacities[activity_idx] = current;
max_past_capacities[activity_idx] = max;

(current, max)
},
);

let current_max = (start_idx..=end_idx).rev().fold(current, |max, activity_idx| {
let max = max
.max_load(*state.get_activity_state(self.feature_keys.current_capacity, activity_idx).unwrap());
state.put_activity_state(self.feature_keys.max_future_capacity, activity_idx, max);
let max = max.max_load(current_capacities[activity_idx]);
max_future_capacities[activity_idx] = max;

max
});

(current - end_pickup, current_max.max_load(max))
});

route_ctx.state_mut().put_activity_states(self.feature_keys.current_capacity, current_capacities);
route_ctx.state_mut().put_activity_states(self.feature_keys.max_past_capacity, max_past_capacities);
route_ctx.state_mut().put_activity_states(self.feature_keys.max_future_capacity, max_future_capacities);

if let Some(capacity) = route_ctx.route().actor.clone().vehicle.dimens.get_capacity() {
route_ctx.state_mut().put_route_state(self.feature_keys.max_load, max_load.ratio(capacity));
}
Expand Down
49 changes: 30 additions & 19 deletions vrp-core/src/construction/features/shared_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,23 +97,27 @@ impl<T: SharedResource> SharedResourceConstraint<T> {
.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, start_idx).and_then(|resource_available| {
let resource_demand = activity_ctx
.target
.job
.as_ref()
.and_then(|job| (self.resource_demand_fn)(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()
}
})
route_ctx
.state()
.get_activity_state::<Option<T>>(self.resource_key, start_idx)
.and_then(|resource_available| *resource_available)
.and_then(|resource_available| {
let resource_demand = activity_ctx
.target
.job
.as_ref()
.and_then(|job| (self.resource_demand_fn)(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()
}
})
})
}
}
Expand Down Expand Up @@ -173,6 +177,8 @@ impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> SharedResourceState<

// second pass: store amount of available resources inside activity state
solution_ctx.routes.iter_mut().for_each(|route_ctx| {
let mut available_resources = vec![None; route_ctx.route().tour.total()];

#[allow(clippy::unnecessary_to_owned)]
(self.interval_fn)(route_ctx).cloned().unwrap_or_default().into_iter().for_each(|(start_idx, _)| {
let resource_available = (self.resource_capacity_fn)(get_activity_by_idx(route_ctx.route(), start_idx))
Expand All @@ -181,14 +187,17 @@ impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> SharedResourceState<
});

if let Some(resource_available) = resource_available {
route_ctx.state_mut().put_activity_state(self.resource_key, start_idx, resource_available);
available_resources[start_idx] = Some(resource_available);
}
});
route_ctx.state_mut().put_activity_states(self.resource_key, available_resources)
});
}

/// Prevents resource consumption in given route by setting available to zero (default).
fn prevent_resource_consumption(&self, route_ctx: &mut RouteContext) {
let mut empty_resources = vec![None; route_ctx.route().tour.total()];

(self.interval_fn)(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)(activity).map_or(false, |(_, _)| {
Expand All @@ -199,9 +208,11 @@ impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> SharedResourceState<
});

if has_resource_demand {
route_ctx.state_mut().put_activity_state(self.resource_key, start_idx, T::default());
empty_resources[start_idx] = Some(T::default());
}
});

route_ctx.state_mut().put_activity_states(self.resource_key, empty_resources)
}

fn get_total_demand(&self, route_ctx: &RouteContext, range: RangeInclusive<usize>) -> T {
Expand Down
8 changes: 5 additions & 3 deletions vrp-core/src/construction/features/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,12 @@ impl TransportObjective {
}

let next = next.unwrap();
let waiting_time = *route_ctx
let waiting_time = route_ctx
.state()
.get_activity_state(self.feature_keys.waiting_time, activity_ctx.index + 1)
.unwrap_or(&0_f64);
.get_activity_states::<Option<Duration>>(self.feature_keys.waiting_time)
.and_then(|states| states.get(activity_ctx.index + 1).copied())
.flatten()
.unwrap_or_default();

let (tp_cost_old, act_cost_old, dep_time_old) =
self.analyze_route_leg(route_ctx, prev, next, prev.schedule.departure);
Expand Down
46 changes: 12 additions & 34 deletions vrp-core/src/construction/heuristics/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ use hashbrown::{HashMap, HashSet};
use nohash_hasher::BuildNoHashHasher;
use rosomaxa::evolution::TelemetryMetrics;
use rosomaxa::prelude::*;
use rustc_hash::FxHasher;
use std::any::Any;
use std::fmt::{Debug, Formatter};
use std::hash::BuildHasherDefault;
use std::ops::Deref;
use std::sync::Arc;

Expand Down Expand Up @@ -248,7 +246,6 @@ pub struct RouteContext {
#[derive(Clone)]
pub struct RouteState {
route_states: HashMap<usize, StateValue, BuildNoHashHasher<usize>>,
activity_states: HashMap<ActivityWithKey, StateValue, BuildHasherDefault<FxHasher>>,
}

impl RouteContext {
Expand Down Expand Up @@ -333,10 +330,7 @@ impl Debug for RouteContext {

impl Default for RouteState {
fn default() -> RouteState {
RouteState {
route_states: HashMap::with_capacity_and_hasher(2, BuildNoHashHasher::<usize>::default()),
activity_states: HashMap::with_capacity_and_hasher(4, BuildHasherDefault::<FxHasher>::default()),
}
RouteState { route_states: HashMap::with_capacity_and_hasher(4, BuildNoHashHasher::<usize>::default()) }
}
}

Expand All @@ -346,44 +340,31 @@ impl RouteState {
self.route_states.get(&key.0).and_then(|s| s.downcast_ref::<T>())
}

/// Gets value associated with key.
pub fn get_route_state_raw(&self, key: StateKey) -> Option<&StateValue> {
self.route_states.get(&key.0)
}

/// Gets value associated with key converted to given type.
pub fn get_activity_state<T: Send + Sync + 'static>(&self, key: StateKey, activity_idx: usize) -> Option<&T> {
self.activity_states.get(&(activity_idx, key.0)).and_then(|s| s.downcast_ref::<T>())
self.route_states
.get(&key.0)
.and_then(|s| s.downcast_ref::<Vec<T>>())
.and_then(|activity_states| activity_states.get(activity_idx))
}

/// Gets value associated with key.
pub fn get_activity_state_raw(&self, key: StateKey, activity_idx: usize) -> Option<&StateValue> {
self.activity_states.get(&(activity_idx, key.0))
/// Gets values associated with key and activities.
pub fn get_activity_states<T: Send + Sync + 'static>(&self, key: StateKey) -> Option<&Vec<T>> {
self.route_states.get(&key.0).and_then(|s| s.downcast_ref::<Vec<T>>())
}

/// Puts value associated with key.
pub fn put_route_state<T: Send + Sync + 'static>(&mut self, key: StateKey, value: T) {
self.route_states.insert(key.0, Arc::new(value));
}

/// Puts value associated with key.
pub fn put_route_state_raw(&mut self, key: StateKey, value: Arc<dyn Any + Send + Sync>) {
self.route_states.insert(key.0, value);
/// Adds values associated with activities.
pub fn put_activity_states<T: Send + Sync + 'static>(&mut self, key: StateKey, values: Vec<T>) {
self.route_states.insert(key.0, Arc::new(values));
}

/// Puts value associated with key and specific activity.
pub fn put_activity_state<T: Send + Sync + 'static>(&mut self, key: StateKey, activity_idx: usize, value: T) {
self.activity_states.insert((activity_idx, key.0), Arc::new(value));
}

/// Puts value associated with key and specific activity.
pub fn put_activity_state_raw(&mut self, key: StateKey, activity_idx: usize, value: StateValue) {
self.activity_states.insert((activity_idx, key.0), value);
}

/// Clear all states, but keeps flags.
/// Clear all states.
pub fn clear(&mut self) {
self.activity_states.clear();
self.route_states.clear();
}
}
Expand Down Expand Up @@ -485,9 +466,6 @@ pub struct ActivityContext<'a> {
pub next: Option<&'a Activity>,
}

/// An internal hash map key represented as `(activity_idx, state_key)`.
type ActivityWithKey = (usize, usize);

/// A local move context.
pub enum MoveContext<'a> {
/// Evaluation of job insertion into the given route.
Expand Down
10 changes: 0 additions & 10 deletions vrp-core/tests/helpers/models/solution/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,6 @@ pub struct RouteStateBuilder {
}

impl RouteStateBuilder {
pub fn add_activity_state<T: Send + Sync + 'static>(
&mut self,
key: StateKey,
activity_idx: usize,
value: T,
) -> &mut Self {
self.state.put_activity_state(key, activity_idx, value);
self
}

pub fn add_route_state<T: Send + Sync + 'static>(&mut self, key: StateKey, value: T) -> &mut Self {
self.state.put_route_state(key, value);
self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ fn can_recede_departure_time_impl(
.build();
let (route, state) = route_ctx.as_mut();
route.tour.get_mut(0).unwrap().schedule.departure = start_departure;
state.put_activity_state::<f64>(schedule_keys.latest_arrival, 1, latest_first_arrival);
state.put_activity_states(schedule_keys.latest_arrival, vec![0., latest_first_arrival]);

if let Some((total, limit)) = total_duration_limit {
state.put_route_state::<f64>(schedule_keys.total_duration, total);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,22 @@ parameterized_test! {can_update_state_for_reserved_time, (vehicle_detail_data, r
can_update_state_for_reserved_time! {
case01_single_outside: ((0, 0, 0., 100.), (25., 30.),
vec![(10, (0., 100.), 10.)],
vec![None, Some(80.), None],
vec![Some(0.), Some(80.), None],
vec![(0., 0.), (10., 20.), (35., 35.)]),

case02_single_inside: ((0, 0, 0., 100.), (25., 30.),
vec![(20, (0., 25.), 10.)],
vec![None, Some(20.), None],
vec![Some(0.), Some(20.), None],
vec![(0., 0.), (20., 35.), (55., 55.)]),

case03_two_inside_travel: ((0, 0, 0., 100.), (25., 30.),
vec![(10, (0., 20.), 10.), (20, (0., 40.), 10.)],
vec![None, Some(15.), Some(40.), None],
vec![Some(0.), Some(15.), Some(40.), None],
vec![(0., 0.), (10., 20.), (35., 45.), (65., 65.)]),

case04_two_inside_service: ((0, 0, 0., 100.), (35., 40.),
vec![(10, (0., 20.), 10.), (20, (0., 50.), 10.)],
vec![None, Some(15.), Some(50.), None],
vec![Some(0.), Some(15.), Some(50.), None],
vec![(0., 0.), (10., 20.), (30., 45.), (65., 65.)]),
}

Expand Down
Loading

0 comments on commit 4d545d0

Please sign in to comment.