diff --git a/CHANGELOG.md b/CHANGELOG.md index ac980fbbc..23ef90f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file. * unexpected total_order behavior in dynamic heuristic (#128) * improve validation rule for break with time offset (#129) * fix issue with skills (#133) +* do not cluster jobs if they are defined in relations (#141) ## [v1.22.1]- 2023-08-26 diff --git a/vrp-core/src/models/problem/jobs.rs b/vrp-core/src/models/problem/jobs.rs index e16570de4..31d61f9e1 100644 --- a/vrp-core/src/models/problem/jobs.rs +++ b/vrp-core/src/models/problem/jobs.rs @@ -243,12 +243,18 @@ impl Jobs { /// Returns range of jobs "near" to given one. Near is defined by costs with relation /// transport profile and departure time. pub fn neighbors(&self, profile: &Profile, job: &Job, _: Timestamp) -> impl Iterator { - self.index.get(&profile.index).unwrap().get(job).unwrap().0.iter().map(|(job, cost)| (job, *cost as f64)) + let index = self.index.get(&profile.index).expect("no profile index"); + let (neighbours_info, _) = index.get(job).expect("no job in profile index"); + + neighbours_info.iter().map(|(job, cost)| (job, *cost as f64)) } /// Returns job rank as relative cost from any vehicle's start position. pub fn rank(&self, profile: &Profile, job: &Job) -> Cost { - self.index.get(&profile.index).unwrap().get(job).unwrap().1 as f64 + let index = self.index.get(&profile.index).expect("no profile index"); + let &(_, cost) = index.get(job).expect("no job in profile index"); + + cost as f64 } /// Returns amount of jobs. diff --git a/vrp-pragmatic/src/format/problem/clustering_reader.rs b/vrp-pragmatic/src/format/problem/clustering_reader.rs index ca2912d22..ab436f21b 100644 --- a/vrp-pragmatic/src/format/problem/clustering_reader.rs +++ b/vrp-pragmatic/src/format/problem/clustering_reader.rs @@ -30,7 +30,7 @@ pub(super) fn create_cluster_config(api_problem: &ApiProblem) -> Result ServingPolicy::Fixed { value, parking }, }, - filtering: get_filter_policy(filtering.as_ref()), + filtering: get_filter_policy(api_problem, filtering.as_ref()), building: get_builder_policy(), })), } @@ -76,16 +76,26 @@ fn get_builder_policy() -> BuilderPolicy { } } -fn get_filter_policy(filtering: Option<&VicinityFilteringPolicy>) -> FilterPolicy { - if let Some(filtering) = filtering { - let excluded_job_ids = filtering.exclude_job_ids.iter().cloned().collect::>(); - FilterPolicy { - job_filter: Arc::new(move |job| { - job.dimens().get_job_id().map_or(true, |job_id| !excluded_job_ids.contains(job_id)) - }), - actor_filter: Arc::new(|_| true), - } +fn get_filter_policy(api_problem: &ApiProblem, filtering: Option<&VicinityFilteringPolicy>) -> FilterPolicy { + let relation_ids = api_problem + .plan + .relations + .iter() + .flat_map(|relations| relations.iter()) + .flat_map(|relation| relation.jobs.iter()) + .cloned() + .collect::>(); + + let excluded_job_ids = if let Some(filtering) = filtering { + filtering.exclude_job_ids.iter().cloned().chain(relation_ids).collect::>() } else { - FilterPolicy { job_filter: Arc::new(|_| true), actor_filter: Arc::new(|_| true) } + relation_ids + }; + + FilterPolicy { + job_filter: Arc::new(move |job| { + job.dimens().get_job_id().map_or(true, |job_id| !excluded_job_ids.contains(job_id)) + }), + actor_filter: Arc::new(|_| true), } } diff --git a/vrp-pragmatic/tests/features/clustering/combination_vicinity_test.rs b/vrp-pragmatic/tests/features/clustering/combination_vicinity_test.rs new file mode 100644 index 000000000..981b20ff7 --- /dev/null +++ b/vrp-pragmatic/tests/features/clustering/combination_vicinity_test.rs @@ -0,0 +1,48 @@ +use super::*; + +parameterized_test! {can_handle_job_in_relation_with_vicinity_cluster, type_field, { + can_handle_job_in_relation_with_vicinity_cluster_impl(type_field); +}} + +can_handle_job_in_relation_with_vicinity_cluster! { + case_01_strict: RelationType::Any, + case_02_sequence: RelationType::Sequence, + case_03_strict: RelationType::Strict, +} + +fn can_handle_job_in_relation_with_vicinity_cluster_impl(type_field: RelationType) { + let problem = Problem { + plan: Plan { + jobs: vec![create_delivery_job("job1", (1., 0.)), create_delivery_job("job2", (1., 0.))], + clustering: Some(Clustering::Vicinity { + profile: VehicleProfile { matrix: "car".to_string(), scale: None }, + threshold: VicinityThresholdPolicy { + duration: 10., + distance: 10., + min_shared_time: None, + smallest_time_window: None, + max_jobs_per_cluster: None, + }, + visiting: VicinityVisitPolicy::Continue, + serving: VicinityServingPolicy::Original { parking: 300. }, + filtering: None, + }), + relations: Some(vec![Relation { + type_field, + jobs: vec!["departure".to_string(), "job1".to_string()], + vehicle_id: "my_vehicle_1".to_string(), + shift_index: None, + }]), + ..create_empty_plan() + }, + fleet: Fleet { vehicles: vec![create_default_vehicle("my_vehicle")], ..create_default_fleet() }, + ..create_empty_problem() + }; + let matrix = create_matrix_from_problem(&problem); + + let solution = solve_with_metaheuristic(problem, Some(vec![matrix])); + + assert!(solution.unassigned.is_none()); + assert_eq!(solution.tours[0].stops.len(), 3); + assert_eq!(solution.tours[0].stops[1].activities().len(), 2); +} diff --git a/vrp-pragmatic/tests/features/clustering/mod.rs b/vrp-pragmatic/tests/features/clustering/mod.rs index 5b98c47e3..f0e291b27 100644 --- a/vrp-pragmatic/tests/features/clustering/mod.rs +++ b/vrp-pragmatic/tests/features/clustering/mod.rs @@ -131,5 +131,6 @@ fn create_test_problem(jobs_data: &[(f64, &str)], capacity: i32, clustering: Clu mod basic_vicinity_test; mod capacity_vicinity_test; +mod combination_vicinity_test; mod profile_vicinity_test; mod specific_vicinity_test;