Skip to content

Commit

Permalink
Add discovery test for recharge feature
Browse files Browse the repository at this point in the history
  • Loading branch information
reinterpretcat committed Jul 22, 2024
1 parent aad9bb0 commit 5d3124b
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 105 deletions.
12 changes: 6 additions & 6 deletions vrp-pragmatic/src/checker/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::cmp::Ordering;
use std::collections::HashSet;
use vrp_core::construction::clustering::vicinity::ServingPolicy;
use vrp_core::models::solution::Place;
use vrp_core::prelude::compare_floats;
use vrp_core::prelude::{compare_floats, GenericResult};
use vrp_core::utils::GenericError;

/// Checks assignment of jobs and vehicles.
Expand All @@ -19,7 +19,7 @@ pub fn check_assignment(ctx: &CheckerContext) -> Result<(), Vec<GenericError>> {
}

/// Checks that vehicles in each tour are used once per shift and they are known in problem.
fn check_vehicles(ctx: &CheckerContext) -> Result<(), GenericError> {
fn check_vehicles(ctx: &CheckerContext) -> GenericResult<()> {
let all_vehicles: HashSet<_> = ctx.problem.fleet.vehicles.iter().flat_map(|v| v.vehicle_ids.iter()).collect();
let mut used_vehicles = HashSet::<(String, usize)>::new();

Expand All @@ -39,7 +39,7 @@ fn check_vehicles(ctx: &CheckerContext) -> Result<(), GenericError> {
}

/// Checks job task rules.
fn check_jobs_presence(ctx: &CheckerContext) -> Result<(), GenericError> {
fn check_jobs_presence(ctx: &CheckerContext) -> GenericResult<()> {
struct JobAssignment {
pub tour_info: (String, usize),
pub pickups: Vec<usize>,
Expand Down Expand Up @@ -122,7 +122,7 @@ fn check_jobs_presence(ctx: &CheckerContext) -> Result<(), GenericError> {
return Err("duplicated job ids in the list of unassigned jobs".into());
}

unique_unassigned_jobs.iter().try_for_each::<_, Result<_, GenericError>>(|job_id| {
unique_unassigned_jobs.iter().try_for_each::<_, GenericResult<_>>(|job_id| {
if !all_jobs.contains_key(job_id) {
return Err(format!("unknown job id in the list of unassigned jobs: '{job_id}'").into());
}
Expand All @@ -149,7 +149,7 @@ fn check_jobs_presence(ctx: &CheckerContext) -> Result<(), GenericError> {
}

/// Checks job constraint violations.
fn check_jobs_match(ctx: &CheckerContext) -> Result<(), GenericError> {
fn check_jobs_match(ctx: &CheckerContext) -> GenericResult<()> {
let (job_index, coord_index) = get_indices(&ctx.core_problem.extras)?;
let (job_index, coord_index) = (job_index.as_ref(), coord_index.as_ref());

Expand Down Expand Up @@ -256,7 +256,7 @@ fn is_valid_job_info(
}
}

fn check_groups(ctx: &CheckerContext) -> Result<(), GenericError> {
fn check_groups(ctx: &CheckerContext) -> GenericResult<()> {
let violations = ctx
.solution
.tours
Expand Down
7 changes: 4 additions & 3 deletions vrp-pragmatic/src/checker/breaks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ mod breaks_test;
use super::*;
use crate::utils::combine_error_results;
use std::iter::once;
use vrp_core::prelude::GenericResult;
use vrp_core::utils::GenericError;

/// Checks that breaks are properly assigned.
pub fn check_breaks(context: &CheckerContext) -> Result<(), Vec<GenericError>> {
combine_error_results(&[check_break_assignment(context)])
}

fn check_break_assignment(context: &CheckerContext) -> Result<(), GenericError> {
fn check_break_assignment(context: &CheckerContext) -> GenericResult<()> {
context.solution.tours.iter().try_for_each(|tour| {
let vehicle_shift = context.get_vehicle_shift(tour)?;
let actual_break_count = tour
Expand All @@ -25,7 +26,7 @@ fn check_break_assignment(context: &CheckerContext) -> Result<(), GenericError>
stop.activities()
.windows(stop.activities().len().min(2))
.flat_map(|leg| as_leg_info_with_break(context, tour, stop, leg))
.try_fold::<_, _, Result<_, GenericError>>(
.try_fold::<_, _, GenericResult<_>>(
acc,
|acc, (from_loc, (from, to), (break_activity, vehicle_break))| {
// check time
Expand Down Expand Up @@ -167,7 +168,7 @@ fn as_leg_info_with_break<'a>(
}

/// Gets break time window.
pub(crate) fn get_break_time_window(tour: &Tour, vehicle_break: &VehicleBreak) -> Result<TimeWindow, GenericError> {
pub(crate) fn get_break_time_window(tour: &Tour, vehicle_break: &VehicleBreak) -> GenericResult<TimeWindow> {
let departure = tour
.stops
.first()
Expand Down
25 changes: 12 additions & 13 deletions vrp-pragmatic/src/checker/capacity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::*;
use crate::utils::combine_error_results;
use std::iter::once;
use vrp_core::models::common::{Load, MultiDimLoad};
use vrp_core::prelude::GenericResult;

/// Checks that vehicle load is assigned correctly. The following rules are checked:
/// * max vehicle's capacity is not violated
Expand All @@ -14,16 +15,16 @@ pub fn check_vehicle_load(context: &CheckerContext) -> Result<(), Vec<GenericErr
combine_error_results(&[check_vehicle_load_assignment(context), check_resource_consumption(context)])
}

fn check_vehicle_load_assignment(context: &CheckerContext) -> Result<(), GenericError> {
context.solution.tours.iter().try_for_each::<_, Result<_, GenericError>>(|tour| {
fn check_vehicle_load_assignment(context: &CheckerContext) -> GenericResult<()> {
context.solution.tours.iter().try_for_each::<_, GenericResult<_>>(|tour| {
let capacity = MultiDimLoad::new(context.get_vehicle(&tour.vehicle_id)?.capacity.clone());
let intervals = get_intervals(context, tour);

intervals
.iter()
.try_fold::<_, _, Result<_, GenericError>>(MultiDimLoad::default(), |acc, interval| {
.try_fold::<_, _, GenericResult<_>>(MultiDimLoad::default(), |acc, interval| {
let (start_delivery, end_pickup) = get_activities_from_interval(context, tour, interval.as_slice())
.try_fold::<_, _, Result<_, GenericError>>(
.try_fold::<_, _, GenericResult<_>>(
(acc, MultiDimLoad::default()),
|acc, (activity, activity_type)| {
let activity_type = activity_type?;
Expand All @@ -37,17 +38,16 @@ fn check_vehicle_load_assignment(context: &CheckerContext) -> Result<(), Generic
},
)?;

let end_capacity = interval.iter().try_fold::<_, _, Result<_, GenericError>>(
start_delivery,
|acc, (idx, (from, to))| {
let end_capacity =
interval.iter().try_fold::<_, _, GenericResult<_>>(start_delivery, |acc, (idx, (from, to))| {
let from_load = MultiDimLoad::new(from.load().clone());
let to_load = MultiDimLoad::new(to.load().clone());

if !capacity.can_fit(&from_load) || !capacity.can_fit(&to_load) {
return Err(format!("load exceeds capacity in tour '{}'", tour.vehicle_id).into());
}

let change = to.activities().iter().try_fold::<_, _, Result<_, GenericError>>(
let change = to.activities().iter().try_fold::<_, _, GenericResult<_>>(
MultiDimLoad::default(),
|acc, activity| {
let activity_type = context.get_activity_type(tour, to, activity)?;
Expand Down Expand Up @@ -80,16 +80,15 @@ fn check_vehicle_load_assignment(context: &CheckerContext) -> Result<(), Generic

Err(format!("load mismatch {} in tour '{}'", message, tour.vehicle_id).into())
}
},
)?;
})?;

Ok(end_capacity - end_pickup)
})
.map(|_| ())
})
}

fn check_resource_consumption(context: &CheckerContext) -> Result<(), GenericError> {
fn check_resource_consumption(context: &CheckerContext) -> GenericResult<()> {
let resources = context
.problem
.fleet
Expand Down Expand Up @@ -170,7 +169,7 @@ fn get_demand(
context: &CheckerContext,
activity: &Activity,
activity_type: &ActivityType,
) -> Result<(DemandType, MultiDimLoad), GenericError> {
) -> GenericResult<(DemandType, MultiDimLoad)> {
let (is_dynamic, demand) = context.visit_job(
activity,
activity_type,
Expand Down Expand Up @@ -235,7 +234,7 @@ fn get_activities_from_interval<'a>(
context: &'a CheckerContext,
tour: &'a Tour,
interval: &'a [(usize, (&Stop, &Stop))],
) -> impl Iterator<Item = (Activity, Result<ActivityType, GenericError>)> + 'a {
) -> impl Iterator<Item = (Activity, GenericResult<ActivityType>)> + 'a {
interval
.iter()
.flat_map(|(_, (from, to))| once(from).chain(once(to)))
Expand Down
50 changes: 45 additions & 5 deletions vrp-pragmatic/src/checker/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ mod limits_test;

use super::*;
use crate::utils::combine_error_results;
use vrp_core::models::common::Distance;
use vrp_core::prelude::GenericResult;

/// NOTE to ensure distance/duration correctness, routing check should be performed first.
pub fn check_limits(context: &CheckerContext) -> Result<(), Vec<GenericError>> {
combine_error_results(&[check_shift_limits(context), check_shift_time(context)])
combine_error_results(&[check_shift_limits(context), check_shift_time(context), check_recharge_limits(context)])
}

/// Check that shift limits are not violated:
/// * max shift time
/// * max distance
fn check_shift_limits(context: &CheckerContext) -> Result<(), GenericError> {
context.solution.tours.iter().try_for_each::<_, Result<_, GenericError>>(|tour| {
fn check_shift_limits(context: &CheckerContext) -> GenericResult<()> {
context.solution.tours.iter().try_for_each::<_, GenericResult<_>>(|tour| {
let vehicle = context.get_vehicle(&tour.vehicle_id)?;

if let Some(ref limits) = vehicle.limits {
Expand Down Expand Up @@ -56,8 +58,8 @@ fn check_shift_limits(context: &CheckerContext) -> Result<(), GenericError> {
})
}

fn check_shift_time(context: &CheckerContext) -> Result<(), GenericError> {
context.solution.tours.iter().try_for_each::<_, Result<_, GenericError>>(|tour| {
fn check_shift_time(context: &CheckerContext) -> GenericResult<()> {
context.solution.tours.iter().try_for_each::<_, GenericResult<_>>(|tour| {
let vehicle = context.get_vehicle(&tour.vehicle_id)?;

let (start, end) = tour.stops.first().zip(tour.stops.last()).ok_or("empty tour")?;
Expand Down Expand Up @@ -87,3 +89,41 @@ fn check_shift_time(context: &CheckerContext) -> Result<(), GenericError> {
}
})
}

fn check_recharge_limits(context: &CheckerContext) -> GenericResult<()> {
context.solution.tours.iter().filter(|tour| tour.stops.len() > 1).try_for_each::<_, GenericResult<_>>(|tour| {
let shift = context.get_vehicle_shift(tour)?;

let Some(recharge) = shift.recharges.as_ref() else { return Ok(()) };

let stops = tour.stops.iter().filter_map(|stop| stop.as_point()).collect::<Vec<_>>();
if stops.len() < 2 {
return Ok(());
}

stops
.windows(2)
.try_fold(Distance::default(), |acc, stops| {
let (prev, next) = match stops {
[prev, next] => (prev, next),
_ => unreachable!(),
};

let delta = (next.distance - prev.distance) as Distance;
let total_distance = acc + delta;

if total_distance > recharge.max_distance {
return Err(format!(
"recharge distance violation: expected limit is {}, got {}, vehicle id '{}', shift index: {}",
recharge.max_distance, total_distance, tour.vehicle_id, tour.shift_index
)
.into());
}

let has_recharge = next.activities.iter().any(|activity| activity.activity_type == "recharge");

Ok(if has_recharge { Distance::default() } else { total_distance })
})
.map(|_| ())
})
}
24 changes: 12 additions & 12 deletions vrp-pragmatic/src/checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use vrp_core::construction::clustering::vicinity::VisitPolicy;
use vrp_core::models::common::{Duration, Profile, TimeWindow};
use vrp_core::models::solution::{Commute as DomainCommute, CommuteInfo as DomainCommuteInfo};
use vrp_core::models::Problem as CoreProblem;
use vrp_core::prelude::GenericError;
use vrp_core::prelude::{GenericError, GenericResult};
use vrp_core::solver::processing::ClusterConfigExtraProperty;

/// Stores problem and solution together and provides some helper methods.
Expand Down Expand Up @@ -96,7 +96,7 @@ impl CheckerContext {
}

/// Gets vehicle by its id.
fn get_vehicle(&self, vehicle_id: &str) -> Result<&VehicleType, GenericError> {
fn get_vehicle(&self, vehicle_id: &str) -> GenericResult<&VehicleType> {
self.problem
.fleet
.vehicles
Expand All @@ -105,7 +105,7 @@ impl CheckerContext {
.ok_or_else(|| format!("cannot find vehicle with id '{vehicle_id}'").into())
}

fn get_vehicle_profile(&self, vehicle_id: &str) -> Result<Profile, GenericError> {
fn get_vehicle_profile(&self, vehicle_id: &str) -> GenericResult<Profile> {
let profile = &self.get_vehicle(vehicle_id)?.profile;
let index = self
.profile_index
Expand Down Expand Up @@ -137,7 +137,7 @@ impl CheckerContext {
}

/// Gets vehicle shift where activity is used.
fn get_vehicle_shift(&self, tour: &Tour) -> Result<VehicleShift, GenericError> {
fn get_vehicle_shift(&self, tour: &Tour) -> GenericResult<VehicleShift> {
let tour_time = TimeWindow::new(
parse_time(
&tour.stops.first().as_ref().ok_or_else(|| "cannot get first activity".to_string())?.schedule().arrival,
Expand Down Expand Up @@ -167,7 +167,7 @@ impl CheckerContext {
}

/// Gets wrapped activity type.
fn get_activity_type(&self, tour: &Tour, stop: &Stop, activity: &Activity) -> Result<ActivityType, GenericError> {
fn get_activity_type(&self, tour: &Tour, stop: &Stop, activity: &Activity) -> GenericResult<ActivityType> {
let shift = self.get_vehicle_shift(tour)?;
let time = self.get_activity_time(stop, activity);
let location = self.get_activity_location(stop, activity);
Expand Down Expand Up @@ -231,7 +231,7 @@ impl CheckerContext {
parking: Duration,
stop: &PointStop,
activity_idx: usize,
) -> Result<Option<DomainCommute>, GenericError> {
) -> GenericResult<Option<DomainCommute>> {
let get_activity_location_by_idx = |idx: usize| {
stop.activities
.get(idx)
Expand Down Expand Up @@ -314,7 +314,7 @@ impl CheckerContext {
activity_type: &ActivityType,
job_visitor: F1,
other_visitor: F2,
) -> Result<R, GenericError>
) -> GenericResult<R>
where
F1: Fn(&Job, &JobTask) -> R,
F2: Fn() -> R,
Expand Down Expand Up @@ -346,13 +346,13 @@ impl CheckerContext {
}
}

fn get_location_index(&self, location: &Location) -> Result<usize, GenericError> {
fn get_location_index(&self, location: &Location) -> GenericResult<usize> {
self.coord_index
.get_by_loc(location)
.ok_or_else(|| format!("cannot find coordinate in coord index: {location:?}").into())
}

fn get_matrix_data(&self, profile: &Profile, from_idx: usize, to_idx: usize) -> Result<(i64, i64), GenericError> {
fn get_matrix_data(&self, profile: &Profile, from_idx: usize, to_idx: usize) -> GenericResult<(i64, i64)> {
let matrices = get_matrices(&self.matrices)?;
let matrix =
matrices.get(profile.index).ok_or_else(|| format!("cannot find matrix with index {}", profile.index))?;
Expand Down Expand Up @@ -414,14 +414,14 @@ fn get_matrix_size(matrices: &[Matrix]) -> usize {
(matrices.first().unwrap().travel_times.len() as f64).sqrt().round() as usize
}

fn get_matrix_value(idx: usize, matrix_values: &[i64]) -> Result<i64, GenericError> {
fn get_matrix_value(idx: usize, matrix_values: &[i64]) -> GenericResult<i64> {
matrix_values
.get(idx)
.cloned()
.ok_or_else(|| format!("attempt to get value out of bounds: {} vs {}", idx, matrix_values.len()).into())
}

fn get_matrices(matrices: &Option<Vec<Matrix>>) -> Result<&Vec<Matrix>, GenericError> {
fn get_matrices(matrices: &Option<Vec<Matrix>>) -> GenericResult<&Vec<Matrix>> {
let matrices = matrices.as_ref().unwrap();

if matrices.iter().any(|matrix| matrix.timestamp.is_some()) {
Expand All @@ -431,7 +431,7 @@ fn get_matrices(matrices: &Option<Vec<Matrix>>) -> Result<&Vec<Matrix>, GenericE
Ok(matrices)
}

fn get_profile_index(problem: &Problem, matrices: &[Matrix]) -> Result<HashMap<String, usize>, GenericError> {
fn get_profile_index(problem: &Problem, matrices: &[Matrix]) -> GenericResult<HashMap<String, usize>> {
let profiles = problem.fleet.profiles.len();
if profiles != matrices.len() {
return Err(format!(
Expand Down
9 changes: 3 additions & 6 deletions vrp-pragmatic/src/checker/relations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ mod relations_test;
use super::*;
use crate::utils::combine_error_results;
use std::collections::HashSet;
use vrp_core::prelude::GenericResult;

/// Checks relation rules.
pub fn check_relations(context: &CheckerContext) -> Result<(), Vec<GenericError>> {
combine_error_results(&[check_relations_assignment(context)])
}

fn check_relations_assignment(context: &CheckerContext) -> Result<(), GenericError> {
fn check_relations_assignment(context: &CheckerContext) -> GenericResult<()> {
let reserved_ids = vec!["departure", "arrival", "break", "reload"].into_iter().collect::<HashSet<_>>();

(0_usize..)
Expand Down Expand Up @@ -94,11 +95,7 @@ fn check_relations_assignment(context: &CheckerContext) -> Result<(), GenericErr
Ok(())
}

fn get_tour_by_vehicle_id(
vehicle_id: &str,
shift_index: Option<usize>,
solution: &Solution,
) -> Result<Tour, GenericError> {
fn get_tour_by_vehicle_id(vehicle_id: &str, shift_index: Option<usize>, solution: &Solution) -> GenericResult<Tour> {
solution
.tours
.iter()
Expand Down
Loading

0 comments on commit 5d3124b

Please sign in to comment.