From c334b486243a8ba16cf2b3b0ec31dee3cfc87b75 Mon Sep 17 00:00:00 2001 From: fonsecadeline Date: Fri, 7 May 2021 11:28:59 +0200 Subject: [PATCH] Use raw vehicle in periodic heuristic --- .../scheduling_data_initialisation.rb | 16 +- .../concerns/scheduling_end_phase.rb | 16 +- lib/heuristics/scheduling_heuristic.rb | 171 +++++++++++------- .../fixtures/compute_shift_route_data.bindump | Bin 739 -> 1452 bytes .../heuristics/scheduling_functions_test.rb | 45 +++-- test/lib/heuristics/scheduling_test.rb | 19 +- test/test_helper.rb | 9 +- test/wrapper_test.rb | 3 +- 8 files changed, 164 insertions(+), 115 deletions(-) diff --git a/lib/heuristics/concerns/scheduling_data_initialisation.rb b/lib/heuristics/concerns/scheduling_data_initialisation.rb index 2b8e94658..1a5bdbd2b 100644 --- a/lib/heuristics/concerns/scheduling_data_initialisation.rb +++ b/lib/heuristics/concerns/scheduling_data_initialisation.rb @@ -24,24 +24,14 @@ module SchedulingDataInitialization def generate_route_structure(vrp) vrp.vehicles.each{ |vehicle| capacity = compute_capacities(vehicle[:capacities], true) - vrp.units.reject{ |unit| capacity.has_key?(unit[:id]) }.each{ |unit| capacity[unit[:id]] = 0.0 } + vrp.units.reject{ |unit| capacity.key?(unit[:id]) }.each{ |unit| capacity[unit[:id]] = 0.0 } @candidate_routes[vehicle.original_id][vehicle.global_day_index] = { vehicle: vehicle, - vehicle_id: vehicle.id, - global_day_index: vehicle.global_day_index, - tw_start: (vehicle.timewindow.start < 84600) ? vehicle.timewindow.start : vehicle.timewindow.start - vehicle.global_day_index * 86400, - tw_end: (vehicle.timewindow.end < 84600) ? vehicle.timewindow.end : vehicle.timewindow.end - vehicle.global_day_index * 86400, - start_point_id: vehicle.start_point&.id, - end_point_id: vehicle.end_point&.id, + tw_start: vehicle.timewindow.start % 84600, + tw_end: vehicle.timewindow.end % 84600, duration: vehicle.duration || (vehicle.timewindow.end - vehicle.timewindow.start), - matrix_id: vehicle.matrix_id, stops: [], - capacity: capacity, capacity_left: Marshal.load(Marshal.dump(capacity)), - maximum_ride_time: vehicle.maximum_ride_time, - maximum_ride_distance: vehicle.maximum_ride_distance, - router_dimension: vehicle.router_dimension.to_sym, - cost_fixed: vehicle.cost_fixed, available_ids: [], completed: false, } diff --git a/lib/heuristics/concerns/scheduling_end_phase.rb b/lib/heuristics/concerns/scheduling_end_phase.rb index c20ac148c..224c417f7 100644 --- a/lib/heuristics/concerns/scheduling_end_phase.rb +++ b/lib/heuristics/concerns/scheduling_end_phase.rb @@ -239,7 +239,9 @@ def empty_underfilled all_empty = false - next if route_data[:stops].sum{ |stop| @services_data[stop[:id]][:raw].exclusion_cost || 0 } >= route_data[:cost_fixed] + next if route_data[:stops].sum{ |stop| + @services_data[stop[:id]][:raw].exclusion_cost.to_f + } >= route_data[:vehicle].cost_fixed smth_removed = true locally_removed = route_data[:stops].collect{ |stop| @@ -248,7 +250,7 @@ def empty_underfilled [stop[:id], stop[:number_in_sequence]] } route_data[:stops] = [] - route_data[:capacity].each{ |unit, qty| + route_data[:vehicle].capacities.each{ |unit, qty| route_data[:capacity_left][unit] = qty } removed += locally_removed @@ -345,7 +347,7 @@ def collect_empty_routes empty_routes << { vehicle_id: vehicle_id, day: day, - stores: [route_data[:start_point_id], route_data[:end_point_id]], + stores: [route_data[:vehicle].start_point&.id, route_data[:vehicle].end_point&.id], time_range: route_data[:tw_end] - route_data[:tw_start] } } @@ -409,7 +411,9 @@ def generate_new_routes(still_removed) if insertion_costs.flatten.empty? keep_inserting = false empty_routes.delete_if{ |tab| tab[:vehicle_id] == best_route[:vehicle_id] && tab[:day] == best_route[:day] } - if route_data[:stops].empty? || route_data[:stops].sum{ |stop| @services_data[stop[:id]][:raw].exclusion_cost || 0 } < route_data[:cost_fixed] + if route_data[:stops].empty? || + route_data[:stops].sum{ |stop| @services_data[stop[:id]][:raw].exclusion_cost || 0 } < + route_data[:vehicle].cost_fixed route_data[:stops] = [] previous_vehicle_filled_info = { stores: best_route[:stores], @@ -420,7 +424,7 @@ def generate_new_routes(still_removed) route_data[:stops].each{ |stop| @ids_to_renumber |= [stop[:id]] still_removed.delete(still_removed.find{ |removed| removed.first == stop[:id] }) - @output_tool&.add_single_visit(route_data[:global_day_index], @services_data[stop[:id]][:used_days], stop[:id], @services_data[stop[:id]][:raw].visits_number) + @output_tool&.add_single_visit(route_data[:vehicle].global_day_index, @services_data[stop[:id]][:used_days], stop[:id], @services_data[stop[:id]][:raw].visits_number) @uninserted.delete(@uninserted.find{ |_id, data| data[:original_id] == stop[:id] }[0]) } end @@ -431,7 +435,7 @@ def generate_new_routes(still_removed) end end else - empty_routes.delete_if{ |tab| tab[:vehicle_id] == best_route[:vehicle_id] && tab[:day] == best_route[:day] } + empty_routes.delete_if{ |tab| tab[:vehicle_id] == best_route[:vehicle].id && tab[:day] == best_route[:day] } end end diff --git a/lib/heuristics/scheduling_heuristic.rb b/lib/heuristics/scheduling_heuristic.rb index 4ac828a6b..d279e9952 100644 --- a/lib/heuristics/scheduling_heuristic.rb +++ b/lib/heuristics/scheduling_heuristic.rb @@ -176,9 +176,16 @@ def check_solution_validity all_routes.each{ |_day, route_data| next if route_data[:stops].empty? - time_back_to_depot = route_data[:stops].last[:end] + matrix(route_data, route_data[:stops].last[:point_id], route_data[:end_point_id]) - raise OptimizerWrapper::SchedulingHeuristicError, 'One vehicle is starting too soon' if route_data[:stops][0][:start] < route_data[:tw_start] - raise OptimizerWrapper::SchedulingHeuristicError, 'One vehicle is ending too late' if time_back_to_depot > route_data[:tw_end] + back_to_depot = route_data[:stops].last[:end] + + matrix(route_data, route_data[:stops].last[:point_id], route_data[:vehicle].end_point&.id) + + if route_data[:stops][0][:start] < route_data[:tw_start] + raise OptimizerWrapper::SchedulingHeuristicError.new('One vehicle is starting too soon') + end + + next unless back_to_depot > route_data[:tw_end] + + raise OptimizerWrapper::SchedulingHeuristicError.new('One vehicle is ending too late') } } @@ -192,7 +199,7 @@ def check_solution_validity s[:arrival].round.between?(compatible_tw[:start], compatible_tw[:end]) && (!@duration_in_tw || s[:end] <= compatible_tw[:end]) - raise OptimizerWrapper::SchedulingHeuristicError, 'One service timewindows violated' + raise OptimizerWrapper::SchedulingHeuristicError.new('One service timewindows violated') } } } @@ -266,7 +273,7 @@ def update_route(route_data, first_index, first_start = nil) route = route_data[:stops] return route if route.empty? || first_index > route.size - previous_id = first_index.zero? ? route_data[:start_point_id] : route[first_index - 1][:id] + previous_id = first_index.zero? ? route_data[:vehicle].start_point&.id : route[first_index - 1][:id] previous_point_id = first_index.zero? ? previous_id : route[first_index - 1][:point_id] previous_end = first_index.zero? ? route_data[:tw_start] : route[first_index - 1][:end] if first_start @@ -284,8 +291,12 @@ def update_route(route_data, first_index, first_start = nil) stop[:end] = stop[:arrival] + @services_data[stop[:id]][:durations][stop[:activity]] stop[:max_shift] = route[position - 1][:max_shift] else - tw = find_corresponding_timewindow(route_data[:global_day_index], previous_end + route_time + stop[:considered_setup_duration], @services_data[stop[:id]][:tws_sets][stop[:activity]], stop[:end] - stop[:arrival]) - raise OptimizerWrapper::SchedulingHeuristicError, 'No timewindow found to update route' if !@services_data[stop[:id]][:tws_sets][stop[:activity]].empty? && tw.nil? + tw = find_corresponding_timewindow(route_data[:vehicle].global_day_index, + previous_end + route_time + stop[:considered_setup_duration], + @services_data[stop[:id]][:tws_sets][stop[:activity]], stop[:end] - stop[:arrival]) + if !@services_data[stop[:id]][:tws_sets][stop[:activity]].empty? && tw.nil? + raise OptimizerWrapper::SchedulingHeuristicError.new('No timewindow found to update route') + end stop[:start] = tw ? [tw[:start] - route_time - stop[:considered_setup_duration], previous_end].max : previous_end stop[:arrival] = stop[:start] + route_time + stop[:considered_setup_duration] @@ -298,8 +309,10 @@ def update_route(route_data, first_index, first_start = nil) previous_end = stop[:end] } - raise OptimizerWrapper::SchedulingHeuristicError, 'Vehicle end violated after updating route' if route.size.positive? && - route.last[:end] + matrix(route_data, route.last[:point_id], route_data[:end_point_id]) > route_data[:tw_end] + if route.any? && + route.last[:end] + matrix(route_data, route.last[:point_id], route_data[:vehicle].end_point&.id) > route_data[:tw_end] + raise OptimizerWrapper::SchedulingHeuristicError.new('Vehicle end violated after updating route') + end route end @@ -400,18 +413,18 @@ def reorder_stops(vrp) next if result.nil? || !result[:unassigned].empty? - time_back_to_depot = route_data[:stops].last[:end] + matrix(route_data, route_data[:stops].last[:point_id], route_data[:end_point_id]) - scheduling_route_time = time_back_to_depot - route_data[:stops].first[:start] - solver_route_time = (result[:routes].first[:activities].last[:begin_time] - result[:routes].first[:activities].first[:begin_time]) # last activity is vehicle depot + back_to_depot = route_data[:stops].last[:end] + + matrix(route_data, route_data[:stops].last[:point_id], route_data[:vehicle].end_point&.id) + scheduling_route_time = back_to_depot - route_data[:stops].first[:start] + solver_route_time = (result[:routes].first[:activities].last[:begin_time] - + result[:routes].first[:activities].first[:begin_time]) # last activity is vehicle depot - next if scheduling_route_time - solver_route_time < @candidate_services_ids.flat_map{ |s| @services_data[s][:durations] }.min || - result[:routes].first[:activities].collect{ |stop| @indices[stop[:service_id]] }.compact == route_data[:stops].collect{ |s| @indices[s[:id]] } # we did not change our points order + minimum_duration = @candidate_services_ids.min_by{ |s| @services_data[s][:durations] } + next if scheduling_route_time - solver_route_time < minimum_duration || + result[:routes].first[:activities].collect{ |stop| @indices[stop[:service_id]] }.compact == + route_data[:stops].collect{ |s| @indices[s[:id]] } # we did not change our points order - begin - route_data[:stops] = compute_route_from(route_data, result[:routes].first[:activities]) # this will change @candidate_routes, but it should not be a problem since OR-tools returns a valid solution - rescue OptimizerWrapper::SchedulingHeuristicError - log 'Failing to construct route from OR-tools solution' - end + route_data[:stops] = compute_route_from(route_data, result[:routes].first[:activities]) } } end @@ -500,8 +513,8 @@ def compute_route_from(new_route, solver_route) end def compute_costs_for_route(route_data, set = nil) - vehicle_id = route_data[:vehicle_id].split('_')[0..-2].join('_') - day = route_data[:vehicle_id].split('_').last.to_i + vehicle_id = route_data[:vehicle].original_id + day = route_data[:vehicle].global_day_index ### compute the cost, for each remaining service to assign, of assigning it to [route_data] ### insertion_costs = [] @@ -584,7 +597,7 @@ def compute_shift(route_data, service_inserted, inserted_final_time, next_servic else next_service[:tw] = @services_data[next_service[:id]][:tws_sets][next_service[:activity]] next_service[:duration] = @services_data[next_service[:id]][:durations][next_service[:activity]] - next_end = compute_tw_for_next(inserted_final_time, next_service, time_to_next, route_data[:global_day_index]) + next_end = compute_tw_for_next(inserted_final_time, next_service, time_to_next, route_data[:vehicle].global_day_index) shift += next_end - next_service[:end] end @@ -661,11 +674,13 @@ def insertion_cost_with_tw(timewindow, route_data, service, position) matrix(route_data, @services_data[next_id][:points_ids][original_next_activity], next_next_point_id) end acceptable_shift, computed_shift = acceptable?(shift, route_data, position) - time_back_to_depot = if position == route_data[:stops].size - timewindow[:final_time] + matrix(route_data, service[:point_id], route_data[:end_point_id]) - else - route_data[:stops].last[:end] + matrix(route_data, route_data[:stops].last[:point_id], route_data[:end_point_id]) + computed_shift - end + time_back_to_depot = + if position == route_data[:stops].size + timewindow[:final_time] + matrix(route_data, service[:point_id], route_data[:vehicle].end_point&.id) + else + route_data[:stops].last[:end] + computed_shift + + matrix(route_data, route_data[:stops].last[:point_id], route_data[:vehicle].end_point&.id) + end end_respected = timewindow[:end_tw] ? timewindow[:arrival_time] <= timewindow[:end_tw] : true route_start = position.zero? ? timewindow[:start_time] : route_data[:stops].first[:start] @@ -686,9 +701,15 @@ def ensure_routes_will_not_be_rejected(vehicle_id, impacted_days) while impacted_days.size.positive? started_day = impacted_days.first can_not_insert_more = false - while @candidate_routes[vehicle_id][started_day][:stops].map{ |stop| @services_data[stop[:id]][:raw].exclusion_cost || 0 }.reduce(&:+) < @candidate_routes[vehicle_id][started_day][:cost_fixed] || can_not_insert_more + route_cost_fixed = @candidate_routes[vehicle_id][started_day][:vehicle].cost_fixed + route_exclusion_costs = + @candidate_routes[vehicle_id][started_day][:stops].map{ |stop| + @services_data[stop[:id]][:raw].exclusion_cost || 0 + }.reduce(&:+) + while route_exclusion_costs < route_cost_fixed || can_not_insert_more inserted_id, _unlocked_ids = try_to_add_new_point(vehicle_id, started_day) impacted_days |= adjust_candidate_routes(vehicle_id, started_day) + route_exclusion_costs += @services_data[inserted_id][:raw].exclusion_cost || 0 if inserted_id can_not_insert_more = true unless inserted_id end @@ -746,7 +767,7 @@ def add_same_freq_located_points(best_index, route_data) additional_durations = @services_data[best_index[:id]][:durations].first + best_index[:considered_setup_duration] @same_located[best_index[:id]].each_with_index{ |service_id, i| @candidate_routes.each{ |_vehicle_id, all_routes| all_routes.each{ |_day, r_d| r_d[:available_ids].delete(service_id) } } - @services_data[service_id][:used_days] << route_data[:global_day_index] + @services_data[service_id][:used_days] << route_data[:vehicle].global_day_index route_data[:stops].insert(best_index[:position] + i + 1, id: service_id, point_id: best_index[:point], @@ -794,8 +815,8 @@ def try_to_add_new_point(vehicle_id, day) def get_previous_info(route_data, position) previous_info = @services_data[route_data[:stops][position - 1][:id]] if position.positive? { - id: position.zero? ? route_data[:start_point_id] : route_data[:stops][position - 1][:id], - point_id: position.zero? ? route_data[:start_point_id] : route_data[:stops][position - 1][:point_id], + id: position.zero? ? route_data[:vehicle].start_point&.id : route_data[:stops][position - 1][:id], + point_id: position.zero? ? route_data[:vehicle].start_point&.id : route_data[:stops][position - 1][:point_id], setup_duration: position.zero? ? 0 : previous_info[:setup_durations][route_data[:stops][position - 1][:activity]], duration: position.zero? ? 0 : previous_info[:durations][route_data[:stops][position - 1][:activity]], tw: position.zero? ? [] : previous_info[:tws_sets][route_data[:stops][position - 1][:activity]], @@ -817,8 +838,8 @@ def best_cost_according_to_tws(route_data, service_id, service_data, previous, o { id: service_id, - vehicle: route_data[:vehicle_id].split('_')[0..-2].join('_'), - day: route_data[:global_day_index], + vehicle: route_data[:vehicle].original_id, + day: route_data[:vehicle].global_day_index, point: service_data[:points_ids][activity], start: tw[:start_time], arrival: tw[:arrival_time], @@ -827,11 +848,13 @@ def best_cost_according_to_tws(route_data, service_id, service_data, previous, o considered_setup_duration: tw[:setup_duration], next_activity: next_activity, potential_shift: tw[:max_shift], - additional_route_time: if route_data[:stops].empty? - matrix(route_data, route_data[:start_point_id], service_data[:points_ids][activity]) + matrix(route_data, service_data[:points_ids][activity], route_data[:end_point_id]) - else - [0, shift - duration - tw[:setup_duration]].max - end, + additional_route_time: + if route_data[:stops].empty? + matrix(route_data, route_data[:vehicle].start_point&.id, service_data[:points_ids][activity]) + + matrix(route_data, service_data[:points_ids][activity], route_data[:vehicle].end_point&.id) + else + [0, shift - duration - tw[:setup_duration]].max + end, back_to_depot: back_depot, activity: activity, } @@ -849,7 +872,7 @@ def compatible_vehicle(service_id, route_data) end def service_compatible_with_route(service_id, route_data) - compatible_days(service_id, route_data[:global_day_index]) && + compatible_days(service_id, route_data[:vehicle].global_day_index) && compatible_vehicle(service_id, route_data) end @@ -865,20 +888,26 @@ def find_best_index(service_id, route_data, first_visit = true) positions_to_try.collect{ |position| ### compute cost of inserting service activity [activity] at [position] in [route_data] previous = get_previous_info(route_data, position) + current = service_data[:points_ids][activity] + next_point = position == route_data[:stops].size ? route_data[:vehicle].end_point&.id : route[position][:point_id] - next_point = (position == route_data[:stops].size) ? route_data[:end_point_id] : route[position][:point_id] - next if position.positive? && position < route.size && # not first neither last position - previous[:point_id] == next_point && previous[:point_id] != @services_data[service_id][:points_ids][activity] # there is no point in testing a position that will imply useless route time + # TODO : this needs to be improved because when we insert a point at same location as start or end_point_id + # we still might create a detour because we can insert at first and/or last position + # we should not generate useless route time : + next if position.between?(1, route.size - 2) && # not first neither last position + matrix(route_data, previous[:point_id], next_point, :time).zero? && + matrix(route_data, previous[:point_id], @services_data[service_id][:points_ids][activity], :time) > 0 next if route_data[:maximum_ride_time] && - (position.positive? && matrix(route_data, previous[:point_id], service_data[:points_ids][activity], :time) > route_data[:maximum_ride_time] || - position < route_data[:stops].size && matrix(route_data, service_data[:points_ids][activity], route[position][:point_id], :time) > route_data[:maximum_ride_time]) + (position.positive? && matrix(route_data, previous, current, :time) > route_data[:maximum_ride_time] || + position < route_data[:stops].size - 2 && matrix(route_data, current, next_point, :time) > route_data[:maximum_ride_time]) next if route_data[:maximum_ride_distance] && - (position.positive? && matrix(route_data, previous[:point_id], service_data[:points_ids][activity], :distance) > route_data[:maximum_ride_distance] || - position < route_data[:stops].size && matrix(route_data, service_data[:points_ids][activity], route[position][:point_id], :distance) > route_data[:maximum_ride_distance]) + (position.postive? && matrix(route_data, previous, current, :distance) > route_data[:maximum_ride_distance] || + position < route_data[:stops].size - 2 && matrix(route_data, current, next_point, :distance) > route_data[:maximum_ride_distance]) - best_cost_according_to_tws(route_data, service_id, service_data, previous, position: position, activity: activity, first_visit: first_visit) + best_cost_according_to_tws(route_data, service_id, service_data, previous, + position: position, activity: activity, first_visit: first_visit) } }.flatten.compact.min_by{ |cost| cost[:back_to_depot] } end @@ -911,7 +940,7 @@ def find_timewindows(previous, inserted_service, route_data) setup_duration: setup_duration } else - inserted_service[:tw].select{ |tw| tw[:day_index].nil? || tw[:day_index] == route_data[:global_day_index] % 7 }.each{ |tw| + inserted_service[:tw].select{ |tw| tw[:day_index].nil? || tw[:day_index] == route_data[:vehicle].global_day_index % 7 }.each{ |tw| start = [previous[:end], tw[:start] - route_time - setup_duration].max arrival = start + route_time + setup_duration final = arrival + inserted_service[:duration] @@ -954,9 +983,13 @@ def insert_point_in_route(route_data, point_to_add, first_visit = true) @services_data[point_to_add[:id]][:used_days] << point_to_add[:day] @services_data[point_to_add[:id]][:used_vehicles] |= [point_to_add[:vehicle]] - @points_vehicles_and_days[point_to_add[:point]][:vehicles] = @points_vehicles_and_days[point_to_add[:point]][:vehicles] | [route_data[:vehicle_id].split('_')[0..-2].join('_')] - @points_vehicles_and_days[point_to_add[:point]][:days] = @points_vehicles_and_days[point_to_add[:point]][:days] | [route_data[:global_day_index]] - @points_vehicles_and_days[point_to_add[:point]][:maximum_visits_number] = [@points_vehicles_and_days[point_to_add[:point]][:maximum_visits_number], @services_data[point_to_add[:id]][:raw].visits_number].max + @points_vehicles_and_days[point_to_add[:point]][:vehicles] = + @points_vehicles_and_days[point_to_add[:point]][:vehicles] | [route_data[:vehicle].original_id] + @points_vehicles_and_days[point_to_add[:point]][:days] = + @points_vehicles_and_days[point_to_add[:point]][:days] | [route_data[:vehicle].global_day_index] + @points_vehicles_and_days[point_to_add[:point]][:maximum_visits_number] = + [@points_vehicles_and_days[point_to_add[:point]][:maximum_visits_number], + @services_data[point_to_add[:id]][:raw].visits_number].max @services_data[point_to_add[:id]][:capacity].each{ |need, qty| route_data[:capacity_left][need] -= qty } @candidate_routes.each{ |_vehicle_id, all_routes| all_routes.each{ |_day, r_d| r_d[:available_ids].delete(point_to_add[:id]) } } if first_visit @@ -992,9 +1025,9 @@ def matrix(route_data, start, arrival, dimension = :time) start = @indices[start] if start.is_a?(String) arrival = @indices[arrival] if arrival.is_a?(String) - return nil unless @matrices.find{ |matrix| matrix[:id] == route_data[:matrix_id] }[dimension] + return nil unless @matrices.find{ |matrix| matrix[:id] == route_data[:vehicle].matrix_id }[dimension] - @matrices.find{ |matrix| matrix[:id] == route_data[:matrix_id] }[dimension][start][arrival] + @matrices.find{ |matrix| matrix[:id] == route_data[:vehicle].matrix_id }[dimension][start][arrival] end end @@ -1033,7 +1066,7 @@ def get_stop(day, vrp, type, vehicle, data = {}) end def collect_route_stops(route_data, day, vrp, vehicle) - previous = route_data[:start_point_id] + previous = route_data[:vehicle].start_point&.id route_data[:stops].collect{ |stop| stop[:travel_time] = matrix(route_data, previous, stop[:point_id]) stop[:travel_distance] = matrix(route_data, previous, stop[:point_id], :distance) @@ -1048,18 +1081,19 @@ def get_route_data(route_data) route_end, final_travel_time, final_travel_distance = if route_data[:stops].empty? - if route_data[:end_point_id] && route_data[:start_point_id] - time_btw_stops = matrix(route_data, route_data[:start_point_id], route_data[:end_point_id]) - distance_btw_stops = matrix(route_data, route_data[:start_point_id], route_data[:end_point_id], :distance) + if route_data[:vehicle].end_point&.id && route_data[:vehicle].start_point&.id + time_btw_stops = matrix(route_data, route_data[:vehicle].start_point&.id, route_data[:vehicle].end_point&.id) + distance_btw_stops = + matrix(route_data, route_data[:vehicle].start_point&.id, route_data[:vehicle].end_point&.id, :distance) [route_start + time_btw_stops, time_btw_stops, distance_btw_stops] else [route_start, 0, 0] end - elsif route_data[:end_point_id] - time_to_end = matrix(route_data, route_data[:stops].last[:point_id], route_data[:end_point_id]) + elsif route_data[:vehicle].end_point&.id + time_to_end = matrix(route_data, route_data[:stops].last[:point_id], route_data[:vehicle].end_point&.id) [route_data[:stops].last[:end] + time_to_end, time_to_end, - matrix(route_data, route_data[:stops].last[:point_id], route_data[:end_point_id], :distance)] + matrix(route_data, route_data[:stops].last[:point_id], route_data[:vehicle].end_point&.id, :distance)] else [route_data[:stops].last[:end], nil, nil] end @@ -1071,17 +1105,17 @@ def get_activities(day, route_data, vrp) computed_activities = [] route_start, route_end, final_travel_time, final_travel_distance = get_route_data(route_data) - vehicle = vrp.vehicles.find{ |v| v.id == route_data[:vehicle_id] } - if route_data[:start_point_id] + vehicle = vrp.vehicles.find{ |v| v.id == route_data[:vehicle].id } + if route_data[:vehicle].start_point&.id computed_activities << get_stop(day, vrp, 'depot', vehicle, - point_id: route_data[:start_point_id], arrival: route_start, travel_time: 0, travel_distance: 0) + point_id: route_data[:vehicle].start_point&.id, arrival: route_start, travel_time: 0, travel_distance: 0) end computed_activities += collect_route_stops(route_data, day, vrp, vehicle) - if route_data[:end_point_id] + if route_data[:vehicle].end_point&.id computed_activities << get_stop(day, vrp, 'depot', vehicle, - point_id: route_data[:end_point_id], arrival: route_end, + point_id: route_data[:vehicle].end_point&.id, arrival: route_end, travel_time: final_travel_time, travel_distance: final_travel_distance) end @@ -1097,12 +1131,12 @@ def prepare_output_and_collect_routes(vrp) computed_activities, start_time, end_time = get_activities(day, route_data, vrp) routes << { - vehicle: { id: route_data[:vehicle_id] }, + vehicle: { id: route_data[:vehicle].id }, mission_ids: computed_activities.collect{ |stop| stop[:service_id] }.compact } solution << { - vehicle_id: route_data[:vehicle_id], + vehicle_id: route_data[:vehicle].id, original_vehicle_id: vehicle_id, start_time: start_time, end_time: end_time, @@ -1188,7 +1222,8 @@ def find_referent(route_data) nil else route_data[:stops].max_by{ |stop| - matrix(route_data, route_data[:start_point_id], stop[:point_id]) + matrix(route_data, stop[:point_id], route_data[:end_point_id]) + matrix(route_data, route_data[:vehicle].start_point&.id, stop[:point_id]) + + matrix(route_data, stop[:point_id], route_data[:vehicle].end_point&.id) }[:point_id] end end diff --git a/test/fixtures/compute_shift_route_data.bindump b/test/fixtures/compute_shift_route_data.bindump index 168f943fab254e2c310b916bbe552f633232ce69..45f6295e27bcc9bdef80e286b075f94c61c31c43 100644 GIT binary patch delta 732 zcmZ8fJ#X7E5ETqpcATWPQ@cR|v;hosPuJ6uDS(F-1uAsOKwPMEeJ~}eBxO4_3jLuh z{Y4$R_cxT3KagbO-Mf$X-tq6N$K!UpyBYm%|7q25vZOgnMH)t*bVkYoeA{MX7~G(9 zhVLs!cJl%HQd=ixyrzsJc-KT3TZe@tkypxbshEt%E*i{n{wyEEWs_f`s&+uV@3?9Z zORg%#%f1MPQqxRWGE0`kabVKKzS>9Um}Tr(7pM+35saT@LuCX5qrDa*a<3G+5>1`F zPp)8WOQP(lgcy#{5Z*;_`4SU5;564xn-xB!&~pamB84NTl}h1lj%%?(wGexExb?5) zx6FCwL}_S7hP-ERme*Wx&Ob@m5{od0OY0Sls{)tE3hxz(3?Hn9?pJa#^ls*8=f9%c zq(7YucBd29;yO;^G&!A4I{C-&gnlq_#YLu98qW8ZO%KU9nQS`Au;wUv!xwOD7p#=M zi@oB3zo_v%H5X39*}nHnh(lX9>^^DW?Oqt>=$WsDUq@?i^BAqwlF@lnt<$9qQ?=pm x98Sd9bn<#f{qt_Z=zu;U(lcb8?@c=4OIi9+1=pJIu~io4_9d1m{rSm9{{c>|{XGBx delta 13 UcmZ3({g{=7g`-+{Bg=gz02_@1)Bpeg diff --git a/test/lib/heuristics/scheduling_functions_test.rb b/test/lib/heuristics/scheduling_functions_test.rb index afa3722e8..d414d97e8 100644 --- a/test/lib/heuristics/scheduling_functions_test.rb +++ b/test/lib/heuristics/scheduling_functions_test.rb @@ -242,7 +242,7 @@ def test_check_validity vrp = VRP.scheduling_seq_timewindows vrp[:services][0][:activity][:timewindows] = [{ start: 100, end: 300 }] vrp = TestHelper.create(vrp) - vrp.vehicles = [] + vrp.vehicles = TestHelper.expand_vehicles(vrp) s = Wrappers::SchedulingHeuristic.new(vrp) s.instance_variable_set(:@candidate_routes, 'vehicle_0' => { @@ -256,7 +256,8 @@ def test_check_validity activity: 0 }], tw_start: 0, - tw_end: 400 + tw_end: 400, + vehicle: vrp.vehicles.first } }) assert s.check_solution_validity @@ -278,7 +279,8 @@ def test_check_validity activity: 0 }], tw_start: 0, - tw_end: 400 + tw_end: 400, + vehicle: vrp.vehicles.first } }) assert_raises OptimizerWrapper::SchedulingHeuristicError do @@ -288,15 +290,16 @@ def test_check_validity def test_compute_next_insertion_cost_when_activities vrp = TestHelper.create(VRP.basic) + vrp.vehicles.each{ |v| v.timewindow = Models::Timewindow.new(start: 0, end: 100000) } vrp.schedule_range_indices = { start: 0, end: 365 } - vrp.vehicles = [] + vrp.vehicles = TestHelper.expand_vehicles(vrp, 'm1') s = Wrappers::SchedulingHeuristic.new(vrp) service = { id: 'service_2', point_id: 'point_2', duration: 0 } timewindow = { start_time: 47517.6, arrival_time: 48559.4, final_time: 48559.4, setup_duration: 0 } route_data = { stops: [{ id: 'service_1', point_id: 'point_1', start: 0, arrival: 47517.6, end: 47517.6, considered_setup_duration: 0, activity: 0, duration: 0 }, - { id: 'service_with_activities', point_id: 'point_1', start: 47517.6, arrival: 47517.6, end: 47517.6, considered_setup_duration: 0, activity: 0, duration: 0 }], - tw_end: 100000, router_dimension: :time, matrix_id: 'm1' } + { id: 'service_with_activities', vehicle: vrp.vehicles.first, point_id: 'point_1', start: 47517.6, arrival: 47517.6, end: 47517.6, considered_setup_duration: 0, activity: 0, duration: 0 }], + tw_end: 100000, router_dimension: :time, vehicle: vrp.vehicles.first, } s.instance_variable_set(:@services_data, Marshal.load(File.binread('test/fixtures/compute_next_insertion_cost_when_activities_services_data.bindump'))) # rubocop: disable Security/MarshalLoad s.instance_variable_set(:@matrices, Marshal.load(File.binread('test/fixtures/compute_next_insertion_cost_when_activities_matrices.bindump'))) # rubocop: disable Security/MarshalLoad s.instance_variable_set(:@indices, Marshal.load(File.binread('test/fixtures/compute_next_insertion_cost_when_activities_indices.bindump'))) # rubocop: disable Security/MarshalLoad @@ -309,7 +312,7 @@ def test_compute_next_insertion_cost_when_activities def test_compute_shift_two_potential_tws vrp = TestHelper.create(VRP.scheduling) - vrp.vehicles = TestHelper.expand_vehicles(vrp) + vrp.vehicles = [] s = Wrappers::SchedulingHeuristic.new(vrp) s.instance_variable_set(:@services_data, Marshal.load(File.binread('test/fixtures/compute_shift_services_data.bindump'))) # rubocop: disable Security/MarshalLoad s.instance_variable_set(:@matrices, Marshal.load(File.binread('test/fixtures/compute_shift_matrices.bindump'))) # rubocop: disable Security/MarshalLoad @@ -321,7 +324,7 @@ def test_compute_shift_two_potential_tws service_to_plan = '1028167_CLI_84_1AB' service_data = s.instance_variable_get(:@services_data)[service_to_plan] route_data[:stops] = [ - { id: '1028167_INI_84_1AB', point_id: '1028167', start: 21600, arrival: 21700, end: 22000, considered_setup_duration: 120, activity: 0 } + { id: '1028167_INI_84_1AB', point_id: '1028167', start: 21600, arrival: 21700, end: 22000, considered_setup_duration: 120, activity: 0, vehicle: vrp.vehicles.first } ] route_data[:stops][0][:point_id] = service_data[:points_ids][0] times_back_to_depot = potential_tws.collect{ |tw| @@ -396,7 +399,7 @@ def test_positions_provided def test_insertion_cost_with_tw_choses_best_value vrp = VRP.lat_lon_scheduling_two_vehicles vrp = TestHelper.create(vrp) - vrp.vehicles = [] + vrp.vehicles = TestHelper.expand_vehicles(vrp) s = Wrappers::SchedulingHeuristic.new(vrp) s.instance_variable_set(:@services_data, 'service_with_activities' => { nb_activities: 2, @@ -409,7 +412,7 @@ def test_insertion_cost_with_tw_choses_best_value s.instance_variable_set(:@indices, Marshal.load(File.binread('test/fixtures/chose_best_value_indices.bindump'))) # rubocop: disable Security/MarshalLoad timewindow = { start_time: 0, arrival_time: 3807, final_time: 3807, setup_duration: 0 } - route_data = { tw_end: 10000, start_point_id: 'point_0', end_point_id: 'point_0', matrix_id: 'm1', + route_data = { tw_end: 10000, start_point_id: 'point_0', end_point_id: 'point_0', matrix_id: 'm1', vehicle: vrp.vehicles.first, stops: [{ id: 'service_with_activities', point_id: 'point_10', start: 0, arrival: 1990, end: 1990, considered_setup_duration: 0, activity: 1 }] } service = { id: 'service_3', point_id: 'point_3', duration: 0 } @@ -419,7 +422,11 @@ def test_insertion_cost_with_tw_choses_best_value def test_empty_underfilled_routes vrp = VRP.lat_lon_scheduling - vrp[:vehicles].each{ |v| v[:cost_fixed] = 2 } + vrp[:vehicles].each{ |v| + v[:cost_fixed] = 2 + v.delete(:start_point_id) + v.delete(:end_point_id) + } vrp[:services].each{ |s| s[:exclusion_cost] = 1 } vrp_to_solve = TestHelper.create(vrp) vrp_to_solve.vehicles = TestHelper.expand_vehicles(vrp_to_solve) @@ -431,10 +438,10 @@ def test_empty_underfilled_routes route_with_four = route_with_three + [{ id: 'service_4', point_id: 'point_4', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }] candidate_route = { 'vehicle_0' => { - 0 => { stops: route_with_one, cost_fixed: 2, global_day_index: 0, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, - 1 => { stops: route_with_two, cost_fixed: 2, global_day_index: 1, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, - 2 => { stops: route_with_three, cost_fixed: 2, global_day_index: 2, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, - 3 => { stops: route_with_four, cost_fixed: 2, global_day_index: 3, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] } + 0 => { stops: route_with_one, global_day_index: 0, tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[0], capacity_left: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, + 1 => { stops: route_with_two, global_day_index: 1, tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[1], capacity_left: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, + 2 => { stops: route_with_three, global_day_index: 2, tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[2], capacity_left: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, + 3 => { stops: route_with_four, global_day_index: 3, tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[3], capacity_left: {}, available_ids: [vrp_to_solve.services.collect(&:id)] } } } @@ -461,10 +468,10 @@ def test_empty_underfilled_routes s.instance_variable_set( :@candidate_routes, 'vehicle_0' => { - 0 => { stops: route_with_four, cost_fixed: 2, global_day_index: 3, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, - 1 => { stops: [{ id: 'service_1', point_id: 'point_1', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }, { id: 'service_2', point_id: 'point_2', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }, { id: 'service_3', point_id: 'point_3', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }], cost_fixed: 2, global_day_index: 2, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, - 2 => { stops: [{ id: 'service_1', point_id: 'point_1', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }, { id: 'service_2', point_id: 'point_2', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }], cost_fixed: 2, global_day_index: 1, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, - 3 => { stops: [{ id: 'service_1', point_id: 'point_1', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }], cost_fixed: 2, global_day_index: 0, tw_start: 0, tw_end: 10000, matrix_id: 'm1', capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, + 0 => { stops: route_with_four, tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[3], capacity_left: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, + 1 => { stops: [{ id: 'service_1', point_id: 'point_1', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }, { id: 'service_2', point_id: 'point_2', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }, { id: 'service_3', point_id: 'point_3', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }], tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[2], capacity_left: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, + 2 => { stops: [{ id: 'service_1', point_id: 'point_1', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }, { id: 'service_2', point_id: 'point_2', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }], tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[1], capacity_left: {}, capacity: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, + 3 => { stops: [{ id: 'service_1', point_id: 'point_1', start: 0, arrival: 0, end: 0, setup_duration: 0, activity: 0 }], tw_start: 0, tw_end: 10000, vehicle: vrp_to_solve.vehicles[0], capacity_left: {}, available_ids: [vrp_to_solve.services.collect(&:id)] }, } ) diff --git a/test/lib/heuristics/scheduling_test.rb b/test/lib/heuristics/scheduling_test.rb index ee38bded7..4d91ffd66 100644 --- a/test/lib/heuristics/scheduling_test.rb +++ b/test/lib/heuristics/scheduling_test.rb @@ -545,26 +545,30 @@ def test_unassign_if_vehicle_not_available_at_provided_day def test_sticky_in_scheduling vrp = VRP.lat_lon_scheduling_two_vehicles result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil) - assert_includes result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } }[:vehicle_id], 'vehicle_0_' # default result + assigned_route = result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } } + assert_includes assigned_route[:vehicle_id], 'vehicle_0_', # default result + 'This test is not consistent anymore, because we use vehicle_1 by default, even without sticky' vrp = VRP.lat_lon_scheduling_two_vehicles vrp[:services].find{ |s| s[:id] == 'service_6' }[:sticky_vehicle_ids] = ['vehicle_1'] result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil) - refute_includes result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } }[:vehicle_id], 'vehicle_0_' + assigned_route = result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } } + assert_includes assigned_route[:vehicle_id], 'vehicle_1_' end def test_skills_in_scheduling vrp = VRP.lat_lon_scheduling_two_vehicles result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil) assigned_route = result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } } - assert_includes assigned_route[:vehicle_id], 'vehicle_0_' # default result + assert_includes assigned_route[:vehicle_id], 'vehicle_0_', # default result + 'This test is not consistent anymore, because we use vehicle_1 by default, even without skills' vrp = VRP.lat_lon_scheduling_two_vehicles vrp[:vehicles][1][:skills] = [[:compatible]] vrp[:services].find{ |s| s[:id] == 'service_6' }[:skills] = [:compatible] result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil) assigned_route = result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } } - refute_includes assigned_route[:vehicle_id], 'vehicle_0_' + assert_includes assigned_route[:vehicle_id], 'vehicle_1_' end def test_with_activities @@ -592,7 +596,8 @@ def test_with_activities result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil) routes_with_activities = result[:routes].select{ |r| r[:activities].collect{ |a| a[:service_id] }.any?{ |id| id&.include?('service_with_activities') } } assert_equal 4, routes_with_activities.size # all activities scheduled (high priority) - assert_equal 1, routes_with_activities.collect{ |r| r[:vehicle_id].split('_').slice(0, 2) }.uniq!&.size # every activity on same vehicle + # every activity on same vehicle : + assert_equal 1, routes_with_activities.collect{ |r| r[:vehicle_id].split('_').slice(0, 2) }.uniq!&.size assert_equal 2, result[:routes].collect{ |r| r[:activities].collect{ |a| a[:service_id]&.include?('service_with_activities') ? a[:point_id] : nil }.compact! }.flatten!&.uniq!&.size end @@ -663,8 +668,8 @@ def test_make_start_tuesday vrp = TestHelper.create(problem) vrp.vehicles = TestHelper.expand_vehicles(vrp) - assert_equal(['vehicle_1', 'vehicle_2', 'vehicle_3'], vrp.vehicles.collect{ |v| v[:id] }) - assert_equal([1, 2, 3], vrp.vehicles.collect{ |v| v[:global_day_index] }) + assert_equal(['vehicle_1', 'vehicle_2', 'vehicle_3'], vrp.vehicles.collect{ |v| v.id }) + assert_equal([1, 2, 3], vrp.vehicles.collect{ |v| v.global_day_index }) s = Wrappers::SchedulingHeuristic.new(vrp) generated_starting_routes = s.instance_variable_get(:@candidate_routes) diff --git a/test/test_helper.rb b/test/test_helper.rb index 689d8fc09..883410b54 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -173,7 +173,14 @@ def self.load_vrps(test, options = {}) end end - def self.expand_vehicles(vrp) + def self.expand_vehicles(vrp, matrix_id = nil) + vrp.vehicles.each{ |v| + v.matrix_id = matrix_id if matrix_id + next if v.timewindow || v.sequence_timewindows.any? + + v.timewindow = Models::Timewindow.new(start: 0, end: 10) + } + periodic = Interpreters::PeriodicVisits.new(vrp) periodic.generate_vehicles(vrp) end diff --git a/test/wrapper_test.rb b/test/wrapper_test.rb index 00c7f96e6..29ee142cf 100644 --- a/test/wrapper_test.rb +++ b/test/wrapper_test.rb @@ -2966,8 +2966,9 @@ def test_empty_result_when_no_vehicle assert_equal expected, result[:unassigned].size # automatically checked within define_process call } + end - # ensure timewindows are returned even if they have work day + def test_returned_timewindows_when_day_index_in_tw vrp = VRP.scheduling vrp[:services][0][:activity][:timewindows] = [{ start: 0, end: 10, day_index: 0 }] vrp[:services][1][:activity][:timewindows] = [{ start: 30, end: 40, day_index: 5 }]