Skip to content

Commit

Permalink
ensure one route per vehicle in result
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsecadeline authored and fonsecadeline committed Sep 14, 2020
1 parent f3a27b2 commit bf5066b
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 218 deletions.
6 changes: 1 addition & 5 deletions lib/filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,7 @@ def self.merge_timewindows(vrp)

def self.filter_skills(vrp)
# Remove duplicate skills and sort
vrp.services.each{ |s|
s.skills = s.skills&.uniq&.sort
}

vrp.shipments.each{ |s|
(vrp.services + vrp.shipments).each{ |s|
s.skills = s.skills&.uniq&.sort
}

Expand Down
14 changes: 2 additions & 12 deletions lib/heuristics/scheduling_heuristic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def initialize(vrp, job = nil)

def compute_initial_solution(vrp, &block)
if @services_data.empty?
empty_result(vrp)
# TODO : create and use result structure instead of using wrapper function
vrp[:preprocessing_heuristic_result] = Wrappers::Wrapper.new.empty_result('heuristic', vrp)
return []
end

Expand Down Expand Up @@ -1026,17 +1027,6 @@ def get_activities(day, vrp, route_activities)
}.flatten
end

def empty_result(vrp)
vrp[:preprocessing_heuristic_result] = {
cost: 0,
solvers: ['heuristic'],
iterations: 0,
routes: [],
unassigned: [],
elapsed: 0.0 # ms
}
end

def prepare_output_and_collect_routes(vrp)
routes = []
solution = []
Expand Down
45 changes: 14 additions & 31 deletions optimizer_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,33 +199,8 @@ def self.solve(service_vrp, job = nil, block = nil)
if !vrp.subtours.empty?
multi_modal = Interpreters::MultiModal.new(vrp, service)
optim_result = multi_modal.multimodal_routes
elsif vrp.vehicles.empty? # TODO: remove ?
optim_result = {
cost: nil,
solvers: [service.to_s],
iterations: nil,
routes: [],
unassigned: vrp.services.collect{ |service_|
{
service_id: service_[:id],
point_id: service_[:activity] ? service_[:activity][:point_id] : nil,
detail: {
lat: service_[:activity] ? service_[:activity][:point][:lat] : nil,
lon: service_[:activity] ? service_[:activity][:point][:lon] : nil,
setup_duration: service_[:activity] ? service_[:activity][:setup_duration] : nil,
duration: service_[:activity] ? service_[:activity][:duration] : nil,
timewindows: (service_[:activity][:timewindows] && !service_[:activity][:timewindows].empty?) ? [{
start: service_[:activity][:timewindows][0][:start],
end: service_[:activity][:timewindows][0][:start],
}] : [],
quantities: service_[:activity] ? service_[:quantities] : nil,
},
reason: 'No vehicle available for this service (split)'
}
},
elapsed: 0,
total_distance: nil
}
elsif vrp.vehicles.empty? || (vrp.services.empty? && vrp.shipments.empty?)
optim_result = Wrappers::Wrapper.new.empty_result(service.to_s, vrp, 'No vehicle available for this service')
else
services_to_reinject = []
sub_unfeasible_services = config[:services][service].detect_unfeasible_services(vrp)
Expand Down Expand Up @@ -351,8 +326,8 @@ def self.split_independent_vrp_by_skills(vrp)
}

mission_skills.size.times.each{ |a_line|
(a_line..mission_skills.size - 1).each{ |b_line|
next if a_line == b_line || (compatibility_table[a_line].select.with_index{ |state, index| state & compatibility_table[b_line][index] }).empty?
((a_line + 1)..mission_skills.size - 1).each{ |b_line|
next if (compatibility_table[a_line].select.with_index{ |state, index| state & compatibility_table[b_line][index] }).empty?

b_set = independent_skills.find{ |set| set.include?(b_line) && set.exclude?(a_line) }
next if b_set.nil?
Expand All @@ -363,14 +338,17 @@ def self.split_independent_vrp_by_skills(vrp)
independent_skills[set_index] += b_set
}
}

# Original skills are retrieved
independant_skill_sets = independent_skills.map{ |index_set|
index_set.collect{ |index| mission_skills[index] }
}

unused_vehicles_indices = (0..vrp.vehicles.size - 1).to_a
independent_vrps = independant_skill_sets.each_with_object([]) { |skills_set, sub_vrps|
# Compatible problem ids are retrieved
vehicles_indices = skills_set.flat_map{ |skills| skill_vehicle_ids.select{ |k, _v| (k & skills) == skills }.flat_map{ |_k, v| v } }.uniq
vehicles_indices.each{ |index| unused_vehicles_indices.delete(index) }
service_ids = skills_set.flat_map{ |skills| skill_service_ids[skills] }
shipment_ids = skills_set.flat_map{ |skills| skill_shipment_ids[skills] }
service_vrp = {
Expand All @@ -385,6 +363,13 @@ def self.split_independent_vrp_by_skills(vrp)
sub_service_vrp[:vrp].resolution_iterations_without_improvment = vrp.resolution_iterations_without_improvment&.*(split_ratio)&.ceil
sub_vrps.push(sub_service_vrp[:vrp])
}

return independent_vrps if unused_vehicles_indices.empty?

sub_service_vrp = Interpreters::SplitClustering.build_partial_service_vrp({ vrp: vrp }, [], unused_vehicles_indices)
sub_service_vrp[:vrp].matrices = []
independent_vrps.push(sub_service_vrp[:vrp])

independent_vrps
end

Expand All @@ -396,8 +381,6 @@ def self.split_independent_vrp_by_sticky_vehicle(vrp)
service_ids = vrp.services.select{ |s| s.sticky_vehicles.map(&:id) == [vehicle_id] }.map(&:id)
shipment_ids = vrp.shipments.select{ |s| s.sticky_vehicles.map(&:id) == [vehicle_id] }.map(&:id)

next if service_ids.empty? && shipment_ids.empty? # No need to create this sub_problem if there is no shipment nor service in it

service_vrp = {
service: nil,
vrp: vrp,
Expand Down
16 changes: 16 additions & 0 deletions test/api/v01/vrp_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,20 @@ def test_transform_route_indice_into_index
assert vrp.routes.first.day_index
assert_equal 10, vrp.routes.first.day_index
end

def test_split_independent_vrp_by_sticky_vehicle
vrp = VRP.independent
vrp[:services] << {
id: 'fake_service',
activity: {
point_id: 'point_2'
},
sticky_vehicle_ids: [
'missing_vehicle_id'
],
}
assert_raises ActiveHash::RecordNotFound do
TestHelper.create(vrp)
end
end
end
21 changes: 18 additions & 3 deletions test/wrapper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3081,21 +3081,36 @@ def test_number_of_service_vrps_generated_in_split_independent
vrp = TestHelper.create(VRP.independent_skills)
vrp.matrices = nil
services_vrps = OptimizerWrapper.split_independent_vrp_by_skills(vrp)
assert_equal 5, services_vrps.size, 'Split_independent_vrp function does not generate expected number of services_vrps'
assert_equal 5, services_vrps.size, 'Split_independent_vrp_by_skills function does not generate expected number of services_vrps'

# add services that can not be served by any vehicle (different configurations)
vrp = TestHelper.create(VRP.independent_skills)
vrp.matrices = nil
vrp.services << Models::Service.new(id: 'fake_service_1', skills: ['fake_skill1'], activity: { point: vrp.points.first })
vrp.services << Models::Service.new(id: 'fake_service_2', skills: ['fake_skill1'], activity: { point: vrp.points.first })
services_vrps = OptimizerWrapper.split_independent_vrp_by_skills(vrp)
assert_equal 6, services_vrps.size, 'Split_independent_vrp function does not generate expected number of services_vrps'
assert_equal 6, services_vrps.size, 'Split_independent_vrp_by_skills function does not generate expected number of services_vrps'

vrp = TestHelper.create(VRP.independent_skills)
vrp.matrices = nil
vrp.services << Models::Service.new(id: 'fake_service_1', skills: ['fake_skill1'], activity: { point: vrp.points.first })
vrp.services << Models::Service.new(id: 'fake_service_3', skills: ['fake_skill2'], activity: { point: vrp.points.first })
services_vrps = OptimizerWrapper.split_independent_vrp_by_skills(vrp)
assert_equal 7, services_vrps.size, 'Split_independent_vrp function does not generate expected number of services_vrps'
assert_equal 7, services_vrps.size, 'Split_independent_vrp_by_skills function does not generate expected number of services_vrps'
end

def test_split_independent_vrps_with_useless_vehicle
vrp = TestHelper.create(VRP.independent_skills)
vrp.vehicles << Models::Vehicle.new(id: 'useless_vehicle')
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil)
assert_equal vrp.vehicles.size, result[:routes].size, 'All vehicles should appear in result, even though they can serve no service'
end

def test_split_independent_vrp_by_sticky_vehicle
vrp = TestHelper.create(VRP.independent)
vrp.vehicles << Models::Vehicle.new(id: 'useless_vehicle')
expected_number_of_vehicles = vrp.vehicles.size
services_vrps = OptimizerWrapper.split_independent_vrp_by_sticky_vehicle(vrp)
assert_equal expected_number_of_vehicles, services_vrps.collect{ |sub_vrp| sub_vrp.vehicles.size }.sum, 'some vehicles disapear because of split_independent_vrp_by_sticky_vehicle function'
end
end
4 changes: 2 additions & 2 deletions test/wrappers/ortools_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5431,7 +5431,7 @@ def test_regroup_timewindows
assert_equal 1, result[:routes][0][:activities].find{ |activity| activity[:service_id] == 'service_1_1_1' }[:detail][:timewindows].size
end

def test_subproblem_with_one_vehicle_and_service
def test_subproblem_with_one_vehicle_and_no_possible_service
problem = {
matrices: [{
id: 'matrix_0',
Expand Down Expand Up @@ -5492,7 +5492,7 @@ def test_subproblem_with_one_vehicle_and_service
vrp = TestHelper.create(problem)
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil)
assert result
assert result[:cost].zero?
assert result[:cost].nil?
end

def test_build_rest
Expand Down
38 changes: 2 additions & 36 deletions wrappers/jsprit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def solver_constraints
:assert_correctness_matrices_vehicles_and_points_definition,
:assert_only_empty_or_fill_quantities,
:assert_points_same_definition,
:assert_at_least_one_mission,
:assert_no_subtours,
:assert_no_evaluation,
:assert_no_first_solution_strategy,
Expand Down Expand Up @@ -74,39 +73,6 @@ def kill

private

def build_timewindows(activity, day_index)
activity.timewindows.select{ |timewindow| timewindow.day_index.nil? || timewindow.day_index == day_index }.collect{ |timewindow|
{
start: timewindow.start,
end: timewindow.end
}
}
end

def build_quantities(job)
job.quantities.collect{ |quantity|
{
unit: quantity.unit.id,
label: quantity.unit.label,
value: quantity.value,
setup_value: quantity.unit.counting ? quantity.setup_value : 0
}
}
end

def build_detail(job, activity, point, day_index)
{
lat: point && point.location && point.location.lat,
lon: point && point.location && point.location.lon,
skills: job.skills,
setup_duration: activity.setup_duration,
duration: activity.duration,
additional_value: activity.additional_value,
timewindows: build_timewindows(activity, day_index),
quantities: build_quantities(job)
}.delete_if{ |_k, v| !v }.compact
end

def assert_end_optimization(vrp)
vrp.resolution_duration || vrp.resolution_iterations || vrp.resolution_iterations_without_improvment
end
Expand Down Expand Up @@ -473,7 +439,7 @@ def parse_output(path, iterations, fleet, vrp)
travel_distance: ((previous_index && point_index && vrp.matrices[0].distance) ? vrp.matrices[0].distance[previous_index][point_index] : 0),
begin_time: (a = act.at_xpath('endTime')) && a && Float(a.content) - duration,
departure_time: (a = act.at_xpath('endTime')) && a && Float(a.content),
detail: build_detail(job, activity, (act['type'] == 'break') ? nil : point, nil)
detail: build_detail(job, activity, (act['type'] == 'break') ? nil : point, nil, nil)
}.delete_if { |_k, v| !v }
previous_index = point_index
current_activity
Expand All @@ -498,7 +464,7 @@ def parse_output(path, iterations, fleet, vrp)
}.delete_if{ |_k, v| !v }] +
(vehicle.rests.empty? ? [nil] : [{
rest_id: vehicle.rests[0].id,
detail: build_detail(rest, rest, nil, nil)
detail: build_detail(rest, rest, nil, nil, nil)
}]) +
[vehicle.end_point && {
point_id: vehicle.end_point.id,
Expand Down
Loading

0 comments on commit bf5066b

Please sign in to comment.