diff --git a/vrp-pragmatic/src/format/problem/goal_reader.rs b/vrp-pragmatic/src/format/problem/goal_reader.rs index 1724b7efe..073cdc869 100644 --- a/vrp-pragmatic/src/format/problem/goal_reader.rs +++ b/vrp-pragmatic/src/format/problem/goal_reader.rs @@ -222,9 +222,15 @@ fn get_objective_feature_layer( } fn get_hierarchical_areas_feature(blocks: &ProblemBlocks, levels: usize) -> GenericResult { - let locations = (0..blocks.transport.size()).collect::>(); let profile = blocks.fleet.profiles.first().cloned().ok_or_else(|| GenericError::from("should have at least one profile"))?; + let locations: Vec<_> = (0..blocks.transport.size()) + .filter(|&loc| { + // Exclude locations with zero distance to all others (no real coordinates) + (0..blocks.transport.size()) + .any(|other| other != loc && blocks.transport.distance_approx(&profile, loc, other) > 0.0) + }) + .collect(); let clusters = create_hierarchical_kmedoids(&locations, levels, { let transport = blocks.transport.clone(); @@ -237,6 +243,13 @@ fn get_hierarchical_areas_feature(blocks: &ProblemBlocks, levels: usize) -> Gene .set_activity_cost(blocks.activity.clone()) .build_minimize_distance()?; + // With too few distinct locations, clustering cannot produce a usable hierarchy + // (the first tier must have exactly two clusters), so fall back to the plain + // distance minimization the feature is based on. + if clusters.first().is_none_or(|clusters| clusters.len() != 2) { + return Ok(cost_feature); + } + create_hierarchical_areas_feature(cost_feature, &clusters, { let transport = blocks.transport.clone(); move |profile, from, to| transport.distance_approx(profile, from, to) diff --git a/vrp-pragmatic/tests/features/tour_shape/mod.rs b/vrp-pragmatic/tests/features/tour_shape/mod.rs index 3c0b8f89a..3bcaf8f98 100644 --- a/vrp-pragmatic/tests/features/tour_shape/mod.rs +++ b/vrp-pragmatic/tests/features/tour_shape/mod.rs @@ -1 +1,2 @@ mod basic_tour_compactness; +mod small_hierarchical_areas; diff --git a/vrp-pragmatic/tests/features/tour_shape/small_hierarchical_areas.rs b/vrp-pragmatic/tests/features/tour_shape/small_hierarchical_areas.rs new file mode 100644 index 000000000..4e55c7ae9 --- /dev/null +++ b/vrp-pragmatic/tests/features/tour_shape/small_hierarchical_areas.rs @@ -0,0 +1,26 @@ +use crate::format::problem::Objective::*; +use crate::format::problem::*; +use crate::helpers::*; + +#[test] +fn can_handle_hierarchical_areas_with_too_few_locations() { + let problem = Problem { + plan: Plan { + jobs: vec![create_delivery_job("job1", (1., 0.)), create_delivery_job("job2", (2., 0.))], + ..create_empty_plan() + }, + fleet: create_default_fleet(), + objectives: Some(vec![ + MinimizeUnassigned { breaks: None }, + HierarchicalAreas { levels: 3 }, + MinimizeCost, + ]), + ..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.len(), 1); +}