diff --git a/lib/interpreters/split_clustering.rb b/lib/interpreters/split_clustering.rb index 39c57c524..ff5532ea5 100644 --- a/lib/interpreters/split_clustering.rb +++ b/lib/interpreters/split_clustering.rb @@ -130,7 +130,7 @@ def self.split_solve_candidate?(service_vrp) vrp.preprocessing_max_split_size && vrp.vehicles.size > 1 && (vrp.resolution_vehicle_limit.nil? || vrp.resolution_vehicle_limit > 1) && - (vrp.shipments.size + vrp.services.size - empties_or_fills.size) > vrp.preprocessing_max_split_size && + (vrp.shipments.size + vrp.services.size - vrp.relations.count{ |r| r.type == 'shipment' } - empties_or_fills.size) > vrp.preprocessing_max_split_size && vrp.shipments.all?{ |s| depot_ids.include?(s.pickup.point_id) || depot_ids.include?(s.delivery.point_id) } else ss_data = service_vrp[:split_solve_data] @@ -139,7 +139,12 @@ def self.split_solve_candidate?(service_vrp) current_vehicles.size > 1 && (ss_data[:current_vehicle_limit].nil? || ss_data[:current_vehicle_limit] > 1) && - current_vehicles.sum{ |v| service_vehicle_assignments[v.id].size } > vrp.preprocessing_max_split_size + current_vehicles.sum{ |vehicle| + service_vehicle_assignments[vehicle.id].size - + service_vehicle_assignments[vehicle.id].count{ |service| + service.relations.any?{ |r| r.type == 'shipment' } + } + } > vrp.preprocessing_max_split_size end end @@ -190,7 +195,9 @@ def self.split_solve(service_vrp, job = nil, &block) # using the service_vehicle_assignments information # (don't generate any sub-VRP yet) split_solve_core(service_vrp, job, &block) # self-recursive method - ensure + rescue StandardError + raise + else service_vrp[:vrp] = service_vrp[:split_solve_data][:original_vrp] service_vrp[:vrp].services.concat empties_or_fills log '<-- split_solve (clustering by max_split)' @@ -908,18 +915,17 @@ def collect_data_items_metrics(vrp, cumulated_metrics, options) } end - # TODO: 0- this part needs to be able to handle real shipments - custom_shipments = build_services_from_shipments(depot_ids, vrp.shipments) + binding_relations = %w[same_route order sequence shipment] - (vrp.services + custom_shipments).group_by{ |s| + vrp.services.group_by{ |s| location = if s.activity s.activity.point.location elsif s.activities.size.positive? - raise UnsupportedProblemError, 'Clustering is not supported yet if one service has serveral activities.' + raise UnsupportedProblemError, 'Clustering does not support services with multiple activities.' end - # TODO: 0- this part needs to be able to handle shipment as a relation + # TODO: 0- this part needs to be able to handle shipment as a relation and not merge the services with binding relations { lat: location.lat.round_with_steps(decimal[:digits], decimal[:steps]), lon: location.lon.round_with_steps(decimal[:digits], decimal[:steps]), @@ -927,7 +933,7 @@ def collect_data_items_metrics(vrp, cumulated_metrics, options) [vrp.routes.find{ |r| r.mission_ids.include? s.id }&.vehicle_id].compact, skills: s.skills.to_a.dup, day_skills: compute_day_skills(s.activity&.timewindows), - id: options[:group_points] ? nil : s.id + id: options[:group_points] && s.relations.all?{ |r| binding_relations.exclude?(r.type) } ? nil : s.id, # use IDs to prevent grouping } }.each_with_index{ |(characteristics, sub_set), sub_set_index| unit_quantities = Hash.new(0) @@ -979,7 +985,10 @@ def zip_dataitems(vrp, items, grouped_objects) c.distance_function = lambda do |data_item_a, data_item_b| # If there is no vehicle that can serve both points at the same time, make sure they are not merged - return max_distance + 1 if (compatible_vehicles[data_item_a[2]] & compatible_vehicles[data_item_b[2]]).empty? + if data_item_a[4][:relations]&.any? || data_item_b[4][:relations]&.any? || + (compatible_vehicles[data_item_a[2]] & compatible_vehicles[data_item_b[2]]).empty? + return max_distance + 1 + end [ vrp.matrices[0][:distance][data_item_a[4][:matrix_index]][data_item_b[4][:matrix_index]], diff --git a/test/lib/interpreters/split_clustering_test.rb b/test/lib/interpreters/split_clustering_test.rb index 4b4d3b7ff..5c14db625 100644 --- a/test/lib/interpreters/split_clustering_test.rb +++ b/test/lib/interpreters/split_clustering_test.rb @@ -618,6 +618,85 @@ def test_max_split_can_handle_empty_vehicles end assert called, 'split_solve_core should have been called' end +# focus + def test_collect_data_items_respects_binding_relations + # create all types of binding relation, all at the same location, and check if they are merged + binding_relations = %w[same_route order sequence shipment] + + problem = VRP.lat_lon + dummy_service = problem[:services].first + problem[:services] = [] + problem[:relations] = [] + binding_relations.each{ |relation| + problem[:relations] << { type: relation, linked_ids: [] } + 2.times.each{ |i| + problem[:services] << Oj.load(Oj.dump(dummy_service)) + problem[:services].last[:id] = "service_#{relation}_#{i}" + problem[:relations].last[:linked_ids] << problem[:services].last[:id] + } + } + + vrp = TestHelper.create(problem) +# byebug + data_items, = Interpreters::SplitClustering.send(:collect_data_items_metrics, vrp, Hash.new(0), { group_points: true, basic_split: false }) + # byebug + assert_equal 7, data_items.size + end + + def test_max_split_can_handle_pud_and_same_route_relations + # Services 1 and 2 are at the same location, as Services 4 and 5. And Services 3 and 6 are far away from tjh + # With Shipments s1{p:1, d:3}, s2{p:2, d:5}, s3{p:4, d:6}, we test that split does the "right" thing; even if, + # it is the "hardest" -- i.e., creating a "costly" split and forcing close points to be on different vehicles. + problem = VRP.lat_lon + 4.times.each{ |i| # have enough vehicles to see if the relations are respected + problem[:vehicles] << problem[:vehicles].first.dup + problem[:vehicles].last[:id] = "vehicle_#{i + 1}" + } + + # problem[:shipments] = [{ # Shipments are only supported as relations at the moment + # id: 'shipment_0', + # pickup: { + # point_id: 'point_1', + # duration: 3, + # late_multiplier: 0, + # }, + # delivery: { + # point_id: 'point_3', + # duration: 3, + # late_multiplier: 0, + # } + # }] + + problem[:relations] = [ + { type: 'shipment', linked_ids: [1, 3].collect{ |i| "service_#{i}" } }, + { type: 'shipment', linked_ids: [2, 5].collect{ |i| "service_#{i}" } }, + { type: 'same_route', linked_ids: [4, 6].collect{ |i| "service_#{i}" } }, + # { type: 'same_route', linked_ids: [1, 4].collect{ |i| "service_#{i}" } } + ] + + # the split should stop even if there are "two services" (which is actually one shipment) + problem[:configuration][:preprocessing][:max_split_size] = 1 + + called = false + Interpreters::SplitClustering.stub(:split_solve_core, lambda{ |service_vrp, _job| + assert_operator service_vrp[:split_level], :<, 3, "split_level shouldn't reach 3. Split candidate should be able to handle binding relations!" + called = true + Interpreters::SplitClustering.send(:__minitest_stub__split_solve_core, service_vrp) # call original function + }) do + OptimizerWrapper.stub(:solve, lambda{ |service_vrp, _job, _block| # stub with empty solution + vrp = service_vrp[:vrp] + # check that only necessary relations are present with all its services + assert_empty vrp.relations.flat_map(&:linked_ids) - vrp.services.collect(&:id), 'Split does not respect relations: Missing binding services or extraneous relations' + service = service_vrp[:service] + OptimizerWrapper.config[:services][service].detect_unfeasible_services(vrp) + OptimizerWrapper.config[:services][service].empty_result(service.to_s, vrp) + }) do + vrp = TestHelper.create(problem) + Interpreters::SplitClustering.split_solve({ service: :ortools, vrp: vrp, dicho_level: 0 }) + end + end + assert called, 'split_solve_core should have been called' + end def test_ignore_debug_parameter_if_no_coordinates vrp = TestHelper.load_vrp(self)