diff --git a/CHANGELOG.md b/CHANGELOG.md index 366f5893b..0b8e66ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Bump VROOM to v1.8.0 and start using the features integrated since v1.3.0 [#107](https://github.com/Mapotempo/optimizer-api/pull/107) - Bump OR-Tools v7.8 [#107](https://github.com/Mapotempo/optimizer-api/pull/107) - VROOM were previously always called synchronously, it is now reserved to a set of effective `router_mode` (:car, :truck_medium) within a limit of points (<200). [#107](https://github.com/Mapotempo/optimizer-api/pull/107) +- Heuristic selection (`first_solution_strategy='self_selection'`) takes into account the supplied initial routes (`routes`) and the best solution is used as the initial route [#159](https://github.com/Mapotempo/optimizer-api/pull/159) ### Removed diff --git a/lib/interpreters/compute_several_solutions.rb b/lib/interpreters/compute_several_solutions.rb index f7e1e7b49..2bdc9160d 100644 --- a/lib/interpreters/compute_several_solutions.rb +++ b/lib/interpreters/compute_several_solutions.rb @@ -82,15 +82,16 @@ def self.collect_heuristics(vrp, first_solution_strategy) if first_solution_strategy.first == 'self_selection' mandatory_heuristic = select_best_heuristic(vrp) - heuristic_list = if vrp[:vehicles].any?{ |vehicle| vehicle[:force_start] || vehicle[:shift_preference] && vehicle[:shift_preference] == 'force_start' } - [mandatory_heuristic, verified('local_cheapest_insertion'), verified('global_cheapest_arc')] - elsif mandatory_heuristic == 'savings' - [mandatory_heuristic, verified('global_cheapest_arc'), verified('local_cheapest_insertion')] - elsif mandatory_heuristic == 'parallel_cheapest_insertion' - [mandatory_heuristic, verified('global_cheapest_arc'), verified('local_cheapest_insertion')] - else - [mandatory_heuristic] - end + heuristic_list = + if vrp[:vehicles].any?{ |vehicle| vehicle[:force_start] || vehicle[:shift_preference].to_s == 'force_start' } + [mandatory_heuristic, verified('local_cheapest_insertion'), verified('global_cheapest_arc')] + elsif mandatory_heuristic == 'savings' + [mandatory_heuristic, verified('global_cheapest_arc'), verified('local_cheapest_insertion')] + elsif mandatory_heuristic == 'parallel_cheapest_insertion' + [mandatory_heuristic, verified('global_cheapest_arc'), verified('local_cheapest_insertion')] + else + [mandatory_heuristic] + end heuristic_list |= ['savings'] if vrp.vehicles.collect{ |vehicle| vehicle[:rests].to_a.size }.sum.positive? # while waiting for self_selection improve heuristic_list @@ -161,20 +162,30 @@ def self.several_solutions(service_vrps) def self.find_best_heuristic(service_vrp) vrp = service_vrp[:vrp] - strategies = vrp.preprocessing_first_solution_strategy - custom_heuristics = collect_heuristics(vrp, strategies) + custom_heuristics = collect_heuristics(vrp, vrp.preprocessing_first_solution_strategy) if custom_heuristics.size > 1 log '---> find_best_heuristic' tic = Time.now percent_allocated_to_heur_selection = 0.3 # spend at most 30% of the total time for heuristic selection total_time_allocated_for_heuristic_selection = service_vrp[:vrp].resolution_duration.to_f * percent_allocated_to_heur_selection - batched_service_vrps = batch_heuristic([service_vrp], custom_heuristics).flatten(1) + time_for_each_heuristic = (total_time_allocated_for_heuristic_selection / custom_heuristics.size).to_i + + custom_heuristics << 'supplied_initial_routes' if vrp.routes.any? + times = [] - first_results = batched_service_vrps.collect{ |s_vrp| + first_results = custom_heuristics.collect{ |heuristic| + s_vrp = Marshal.load(Marshal.dump(service_vrp)) + if heuristic == 'supplied_initial_routes' + s_vrp[:vrp].preprocessing_first_solution_strategy = [verified('global_cheapest_arc')] # fastest for fallback + else + s_vrp[:vrp].routes = [] + s_vrp[:vrp].preprocessing_first_solution_strategy = [verified(heuristic)] + end + s_vrp[:vrp].restitution_allow_empty_result = true s_vrp[:vrp].resolution_batch_heuristic = true s_vrp[:vrp].resolution_initial_time_out = nil s_vrp[:vrp].resolution_minimum_duration = nil - s_vrp[:vrp].resolution_duration = [(total_time_allocated_for_heuristic_selection / custom_heuristics.size).to_i, 300000].min # do not spend more than 5 min for a single heuristic + s_vrp[:vrp].resolution_duration = [time_for_each_heuristic, 300000].min # no more than 5 min for single heur heuristic_solution = OptimizerWrapper.solve(s_vrp) times << (heuristic_solution && heuristic_solution[:elapsed] || 0) heuristic_solution @@ -185,30 +196,40 @@ def self.find_best_heuristic(service_vrp) synthesis = [] first_results.each_with_index{ |result, i| synthesis << { - heuristic: batched_service_vrps[i][:vrp].preprocessing_first_solution_strategy.first, - quality: result.nil? ? nil : result[:cost].to_i + (times[i] / 1000).to_i, + heuristic: custom_heuristics[i], + quality: result.nil? ? Float::MAX : result[:cost].to_i + (times[i] / 1000).to_i, used: false, cost: result ? result[:cost] : nil, time_spent: times[i], solution: result } } - sorted_heuristics = synthesis.sort_by{ |element| element[:quality].nil? ? synthesis.collect{ |data| data[:quality] }.compact.max * 10 : element[:quality] } - - best_heuristic = sorted_heuristics[0][:heuristic] + best = synthesis.min_by{ |element| element[:quality] } + + if best[:heuristic] != 'supplied_initial_routes' + # if another heuristic is the best, use its solution as the initial route + vrp.routes = best[:solution][:routes].collect{ |route| + mission_ids = route[:activities].collect{ |a| + a[:service_id] || a[:pickup_shipment_id] || a[:delivery_shipment_id] + }.compact + next if mission_ids.empty? + + Models::Route.new(vehicle: vrp.vehicles.find{ |v| v[:id] = route[:vehicle_id] }, mission_ids: mission_ids) + }.compact + end - synthesis.find{ |heur| heur[:heuristic] == best_heuristic }[:used] = true + best[:used] = true - vrp.preprocessing_heuristic_result = synthesis.find{ |heur| heur[:heuristic] == best_heuristic }[:solution] + vrp.preprocessing_heuristic_result = best[:solution] vrp.preprocessing_heuristic_result[:solvers].each{ |solver| solver = 'preprocessing_' + solver } synthesis.each{ |synth| synth.delete(:solution) } vrp.resolution_batch_heuristic = nil - vrp.preprocessing_first_solution_strategy = [best_heuristic] + vrp.preprocessing_first_solution_strategy = [best[:heuristic]] vrp.preprocessing_heuristic_synthesis = synthesis vrp.resolution_duration = vrp.resolution_duration ? [(vrp.resolution_duration.to_f * (1 - percent_allocated_to_heur_selection)).round, 1000].max : nil - log "<--- find_best_heuristic elapsed: #{Time.now - tic}sec selected heuristic: #{best_heuristic}" + log "<--- find_best_heuristic elapsed: #{Time.now - tic}sec selected heuristic: #{best[:heuristic]}" else vrp.preprocessing_first_solution_strategy = custom_heuristics end