From f6f6680510b6b444388546e1169b64aac0f888ce Mon Sep 17 00:00:00 2001 From: fonsecadeline Date: Thu, 10 Jun 2021 17:22:26 +0200 Subject: [PATCH] Multi-tour with schedule --- lib/interpreters/periodic_visits.rb | 108 ++++++++++------------ models/concerns/validate_data.rb | 2 +- test/lib/interpreters/interpreter_test.rb | 42 ++++++++- test/models/vrp_consistency_test.rb | 28 ++++++ 4 files changed, 118 insertions(+), 62 deletions(-) diff --git a/lib/interpreters/periodic_visits.rb b/lib/interpreters/periodic_visits.rb index ae0c8a881..6d4effd1e 100644 --- a/lib/interpreters/periodic_visits.rb +++ b/lib/interpreters/periodic_visits.rb @@ -41,11 +41,7 @@ def initialize(vrp) def expand(vrp, job, &block) return vrp unless vrp.scheduling? - vehicles_linked_by_duration = save_relations(vrp, :vehicle_group_duration).concat( - save_relations(vrp, :vehicle_group_duration_on_weeks) - ).concat( - save_relations(vrp, :vehicle_group_duration_on_months) - ) + vehicles_linking_relations = save_vehicle_linking_relations(vrp) vrp.relations = generate_relations(vrp) vrp.rests = [] vrp.vehicles = generate_vehicles(vrp).sort{ |a, b| @@ -61,8 +57,7 @@ def expand(vrp, job, &block) vrp.shipments = generate_shipments(vrp) @periods.uniq! - vehicles_linked_by_duration = get_all_vehicles_in_relation(vehicles_linked_by_duration) - generate_relations_on_periodic_vehicles(vrp, vehicles_linked_by_duration) + generate_relations_on_periodic_vehicles(vrp, vehicles_linking_relations) if vrp.preprocessing_first_solution_strategy.to_a.first != 'periodic' && vrp.services.any?{ |service| service.visits_number > 1 } vrp.routes = generate_routes(vrp) @@ -247,7 +242,6 @@ def generate_vehicles(vrp) timewindows = [vehicle.timewindow || vehicle.sequence_timewindows].flatten if timewindows.empty? new_vehicle = build_vehicle(vrp, vehicle, vehicle_day_index, rests_durations) - @equivalent_vehicles[vehicle.original_id] << new_vehicle.id new_vehicle else timewindows.select{ |timewindow| timewindow.day_index.nil? || timewindow.day_index == vehicle_day_index % 7 }.collect{ |associated_timewindow| @@ -456,67 +450,63 @@ def compute_possible_days(vrp) } end - def save_relations(vrp, relation_type) - vrp.relations.select{ |r| r.type == relation_type }.collect{ |r| - { - type: r.type, - linked_vehicle_ids: r.linked_vehicle_ids, - lapse: r.lapse, - periodicity: r.periodicity - } + def save_vehicle_linking_relations(vrp) + vrp.relations.select{ |r| + [:vehicle_group_duration, :vehicle_group_duration_on_weeks, :vehicle_group_duration_on_months, + :vehicle_trips].include?(r.type) } end - def get_all_vehicles_in_relation(relations) - relations&.each{ |r| - next if r[:type] == :vehicle_group_duration + def cut_linking_vehicle_relation_by_period(relation, periods, relation_type) + additional_relations = [] + vehicles_in_relation = + relation[:linked_vehicle_ids].flat_map{ |v| @equivalent_vehicles[v] } - new_list = [] - r[:linked_vehicle_ids].each{ |v| - new_list.concat(@equivalent_vehicles[v]) - } - r[:linked_vehicle_ids] = new_list - } + while periods.any? + days_in_period = periods.slice!(0, relation.periodicity).flatten + relation_vehicles = vehicles_in_relation.select{ |id| days_in_period.include?(id.split('_').last.to_i) } + next unless relation_vehicles.any? + + additional_relations << Models::Relation.new( + linked_vehicle_ids: relation_vehicles, + lapse: relation.lapse, + type: relation_type + ) + end + + additional_relations end - def generate_relations_on_periodic_vehicles(vrp, list) - new_relations = [] - list.each{ |r| - case r[:type] + def collect_weeks_in_schedule + current_day = (@schedule_start + 1..@schedule_start + 7).find{ |d| d % 7 == 0 } # next monday + schedule_week_indices = [(@schedule_start..current_day - 1).to_a] + while current_day + 6 <= @schedule_end + schedule_week_indices << (current_day..current_day + 6).to_a + current_day += 7 + end + schedule_week_indices << (current_day..@schedule_end).to_a unless current_day > @schedule_end + + schedule_week_indices + end + + def generate_relations_on_periodic_vehicles(vrp, vehicle_linking_relations) + vrp.relations.concat(vehicle_linking_relations.flat_map{ |relation| + case relation[:type] when :vehicle_group_duration - new_relations << [r[:linked_vehicle_ids], r[:lapse]] + Models::Relation.new( + type: :vehicle_group_duration, lapse: relation.lapse, + linked_vehicle_ids: relation[:linked_vehicle_ids].flat_map{ |v| @equivalent_vehicles[v] }) when :vehicle_group_duration_on_weeks - current_sub_list = [] - first_index = r[:linked_vehicle_ids].min.split('_').last.to_i - in_periodicity = first_index + 7 * (r[:periodicity] - 1) - max_index = (in_periodicity..in_periodicity + 7).find{ |index| index % 7 == 6 } - r[:linked_vehicle_ids].sort_by{ |v_id| v_id.split('_').last.to_i }.each{ |v_id| - this_index = v_id.split('_').last.to_i - if this_index <= max_index - current_sub_list << v_id - else - new_relations << [current_sub_list, r[:lapse]] - current_sub_list = [v_id] - first_index = this_index - in_periodicity = first_index + 7 * (r[:periodicity] - 1) - max_index = (in_periodicity..in_periodicity + 7).find{ |index| index % 7 == 6 } - end - } - new_relations << [current_sub_list, r[:lapse]] + schedule_week_indices = collect_weeks_in_schedule + cut_linking_vehicle_relation_by_period(relation, schedule_week_indices, :vehicle_group_duration) when :vehicle_group_duration_on_months - (0..vrp.schedule_months_indices.size - 1).step(r[:periodicity]).collect{ |v| p vrp.schedule_months_indices.slice(v, v + r[:periodicity]).flatten }.each{ |month_indices| - new_relations << [r[:linked_vehicle_ids].select{ |id| month_indices.include?(id.split('_').last.to_i) }, r[:lapse]] - } + cut_linking_vehicle_relation_by_period(relation, vrp.schedule_months_indices, :vehicle_group_duration) + when :vehicle_trips + # we want want vehicle_trip relation per day : + all_days = (@schedule_start..@schedule_end).to_a + cut_linking_vehicle_relation_by_period(relation, all_days, :vehicle_trips) end - } - - new_relations.each{ |linked_vehicle_ids, lapse| - vrp.relations << Models::Relation.new( - type: :vehicle_group_duration, - linked_vehicle_ids: linked_vehicle_ids, - lapse: lapse - ) - } + }) end private diff --git a/models/concerns/validate_data.rb b/models/concerns/validate_data.rb index 49550f90e..802dfdff0 100644 --- a/models/concerns/validate_data.rb +++ b/models/concerns/validate_data.rb @@ -145,7 +145,7 @@ def calculate_day_availabilities(vehicles, timewindow_arrays) if timewindows.empty? [] else - days = timewindows.flat_map{ |tw| tw[:day_index] } + days = timewindows.flat_map{ |tw| tw[:day_index] || (0..6).to_a } days.compact! days.uniq end diff --git a/test/lib/interpreters/interpreter_test.rb b/test/lib/interpreters/interpreter_test.rb index ff0b3b87d..cae60874f 100644 --- a/test/lib/interpreters/interpreter_test.rb +++ b/test/lib/interpreters/interpreter_test.rb @@ -1408,7 +1408,7 @@ def test_overall_duration_several_vehicles assert_equal 2, expanded_vrp.relations.size problem[:relations].first[:type] = :vehicle_group_duration - expanded_vrp = periodic_expand(vrp) + expanded_vrp = periodic_expand(problem) assert_equal 1, expanded_vrp.relations.size assert_equal 4, expanded_vrp.relations.first[:linked_vehicle_ids].size @@ -1418,7 +1418,7 @@ def test_overall_duration_several_vehicles } vrp = TestHelper.create(problem) refute_empty vrp.schedule_months_indices - expanded_vrp = periodic.send(:expand, vrp, nil) + expanded_vrp = periodic_expand(problem) assert_equal 2, expanded_vrp.relations.size end @@ -1484,6 +1484,9 @@ def test_expand_relations_of_one_month_and_one_day expanded_vrp = periodic_expand(problem) assert_equal 2, expanded_vrp.relations.size + problem[:configuration][:schedule] = { + range_date: { start: Date.new(2020, 1, 1), end: Date.new(2020, 2, 1) } + } problem[:relations].first[:periodicity] = 2 expanded_vrp = periodic_expand(problem) assert_equal 1, expanded_vrp.relations.size @@ -1554,4 +1557,39 @@ def test_first_last_possible_days assert_equal 2, expanded_vrp.services.first.first_possible_days[0] assert_equal 2, expanded_vrp.services.first.last_possible_days[0] end + + def test_expand_multitrips_relations + # If two vehicles are in vehicle_trips relation, + # VRP is only valid if they available at same days + # In this case, vehicle_trip relation should be repeated + # every day those vehicles are available + vrp = VRP.lat_lon_two_vehicles + vrp[:configuration][:schedule] = { range_indices: { start: 0, end: 3 }} + vrp[:relations] = [TestHelper.vehicle_trips_relation(vrp)] + vrp[:vehicles].each{ |v| + v.delete(:sequence_timewindows) + v[:timewindow] = { start: 0, end: 10 } + } + expanded_vrp = periodic_expand(vrp) + assert_equal 8, expanded_vrp.vehicles.size + assert_equal 4, (expanded_vrp.relations.count{ |r| r[:type] == :vehicle_trips }) + + vrp[:vehicles].each{ |v| + v[:timewindow] = { start: 0, end: 10, day_index: 0 } + } + expanded_vrp = periodic_expand(vrp) + assert_equal 2, expanded_vrp.vehicles.size + assert_equal 1, (expanded_vrp.relations.count{ |r| r[:type] == :vehicle_trips }) + + vrp[:vehicles].each{ |v| + v.delete(:timewindow) + } + vrp[:vehicles][0][:sequence_timewindows] = [{ start: 0, end: 10, day_index: 0 }, + { start: 20, end: 30, day_index: 1 }] + vrp[:vehicles][1][:sequence_timewindows] = [{ start: 5, end: 15, day_index: 0 }, + { start: 17, end: 45, day_index: 1 }] + expanded_vrp = periodic_expand(vrp) + assert_equal 4, expanded_vrp.vehicles.size + assert_equal 2, (expanded_vrp.relations.count{ |r| r[:type] == :vehicle_trips }) + end end diff --git a/test/models/vrp_consistency_test.rb b/test/models/vrp_consistency_test.rb index 3b85a3921..d7b4abf47 100644 --- a/test/models/vrp_consistency_test.rb +++ b/test/models/vrp_consistency_test.rb @@ -473,6 +473,34 @@ def test_compatible_days_availabilities_with_vehicle_trips end end + def test_compatible_days_availabilities_with_vehicle_trips_with_sequence_timewindows + vrp = VRP.lat_lon_two_vehicles + vrp[:configuration][:schedule] = { range_indices: { start: 0, end: 3 }} + vrp[:relations] = [TestHelper.vehicle_trips_relation(vrp)] + vrp[:vehicles][0][:sequence_timewindows] = [{ start: 0, end: 10, day_index: 0 }, + { start: 20, end: 30, day_index: 1 }] + vrp[:vehicles][1][:sequence_timewindows] = [{ start: 5, end: 15, day_index: 0 }, + { start: 17, end: 45, day_index: 1 }] + + check_consistency(vrp) # this should not raise + + vrp[:vehicles][1][:sequence_timewindows] = [{ start: 5, end: 15, day_index: 0 }] + assert_raises OptimizerWrapper::UnsupportedProblemError do + check_consistency(vrp) + end + + vrp[:vehicles][1][:sequence_timewindows] = [{ start: 5, end: 15 }] + assert_raises OptimizerWrapper::UnsupportedProblemError do + check_consistency(vrp) + end + + vrp[:vehicles][1][:sequence_timewindows] = [{ start: 5, end: 15, day_index: 0 }, + { start: 17, end: 19, day_index: 1 }] + assert_raises OptimizerWrapper::DiscordantProblemError do + check_consistency(vrp) + end + end + def test_unavailable_days_with_vehicle_trips vrp = VRP.lat_lon_scheduling_two_vehicles vrp[:relations] = [TestHelper.vehicle_trips_relation(vrp)]