Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: dicho cannot find matrix #299

Merged
merged 5 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 51 additions & 20 deletions lib/heuristics/dichotomious_approach.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,10 @@ def self.dichotomious_heuristic(service_vrp, job = nil, &block)
if sub_service_vrp[:vrp].resolution_minimum_duration
sub_service_vrp[:vrp].resolution_minimum_duration *= sub_service_vrp[:vrp].services.size / service_vrp[:vrp].services.size.to_f * 2
end
matrix_indices = sub_service_vrp[:vrp].points.map{ |point|
service_vrp[:vrp].points.find{ |r_point| point.id == r_point.id }.matrix_index
}
SplitClustering.update_matrix_index(sub_service_vrp[:vrp])
SplitClustering.update_matrix(service_vrp[:vrp].matrices, sub_service_vrp[:vrp], matrix_indices)

result = OptimizerWrapper.define_process(sub_service_vrp, job, &block)

transfer_unused_vehicles(result, sub_service_vrps) if index.zero? && result
transfer_unused_vehicles(service_vrp, result, sub_service_vrps) if index.zero? && result

result
}
Expand Down Expand Up @@ -129,27 +125,23 @@ def self.dichotomious_heuristic(service_vrp, job = nil, &block)
result
end

def self.transfer_unused_vehicles(result, sub_service_vrps)
def self.transfer_unused_vehicles(service_vrp, result, sub_service_vrps)
original_vrp = service_vrp[:vrp]
sv_zero = sub_service_vrps[0][:vrp]
sv_one = sub_service_vrps[1][:vrp]

# First transfer empty vehicles that appear in the routes
result[:routes].each{ |r|
next if !r[:activities].select{ |a| a[:service_id] }.empty?

vehicle = sv_zero.vehicles.find{ |v| v.id == r[:vehicle_id] }
sv_one.vehicles << vehicle
sv_zero.vehicles -= [vehicle]
sv_one.points += sv_zero.points.select{ |p| p.id == vehicle.start_point_id || p.id == vehicle.end_point_id }
}

# Then transfer the vehicles which do not appear in the routes.
original_matrix_indices = nil
sv_zero.vehicles.each{ |vehicle|
next if result[:routes].any?{ |r| r[:vehicle_id] == vehicle.id }
route = result[:routes].find{ |r| r[:vehicle_id] == vehicle.id }

# Transfer the vehicles which do not appear in the routes or the empty vehicles that appear in the routes
next if route && !route[:activities].select{ |a| a[:service_id] }.empty?

sv_one.vehicles << vehicle
sv_zero.vehicles -= [vehicle]
sv_one.points += sv_zero.points.select{ |p| p.id == vehicle.start_point_id || p.id == vehicle.end_point_id }
vehicle_points = sv_zero.points.select{ |p| p.id == vehicle.start_point_id || p.id == vehicle.end_point_id }

update_sv_one_matrix(sv_one, original_vrp, original_matrix_indices, vehicle, vehicle_points)
}

# Transfer unsued vehicle limit to the other side as well
Expand All @@ -158,6 +150,45 @@ def self.transfer_unused_vehicles(result, sub_service_vrps)
sv_one.resolution_vehicle_limit += sv_zero_unused_vehicle_limit
end

def self.update_sv_one_matrix(sv_one, original_vrp, original_matrix_indices, vehicle, vehicle_points)
vehicle_points.each{ |new_point|
point_exists = sv_one.points.find{ |p| p.id == new_point.id }

if point_exists
vehicle.start_point = point_exists if vehicle.start_point_id == new_point.id
vehicle.end_point = point_exists if vehicle.end_point_id == new_point.id
next
end

new_point.matrix_index = sv_one.points.size

original_matrix_indices ||= sv_one.points.map{ |p| original_vrp.points.find{ |pi| pi.id == p.id }.matrix_index }
new_point_original_matrix_index = original_vrp.points.find{ |pi| pi.id == new_point.id }.matrix_index

# Update the matrix
sv_one.matrices.each_with_index{ |sv_one_matrix, m_index|
%i[time distance value].each{ |dimension|
d_matrix = sv_one_matrix.send(dimension)
next unless d_matrix

original_matrix = original_vrp.matrices[m_index].send(dimension)

# existing points to new_point
d_matrix.each_with_index{ |row, r_index|
row << original_matrix[original_matrix_indices[r_index]][new_point_original_matrix_index]
}
# new_point to existing points
d_matrix << original_matrix[new_point_original_matrix_index].values_at(*original_matrix_indices)
# new_point to new_point
d_matrix.last << 0
}
}

original_matrix_indices << new_point_original_matrix_index
sv_one.points << new_point
}
end

def self.dicho_level_coeff(service_vrp)
balance = 0.66666
level_approx = Math.log(service_vrp[:vrp].resolution_dicho_division_vehicle_limit / (service_vrp[:vrp].resolution_vehicle_limit || service_vrp[:vrp].vehicles.size).to_f, balance)
Expand Down
37 changes: 23 additions & 14 deletions lib/interpreters/split_clustering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,13 @@ def self.remove_poorly_populated_routes(vrp, result, limit)
log 'Some routes are emptied due to poor workload -- time or quantity.', level: :warn if emptied_routes
end

def self.update_matrix(original_matrices, sub_vrp, matrix_indices)
sub_vrp.matrices.each_with_index{ |matrix, index|
[:time, :distance].each{ |dimension|
matrix[dimension] = sub_vrp.vehicles.first.matrix_blend(original_matrices[index], matrix_indices, [dimension], cost_time_multiplier: 1, cost_distance_multiplier: 1)
def self.update_matrix(sub_vrp, matrix_indices)
sub_vrp.matrices.each{ |matrices|
[:time, :distance, :value].each{ |dimension|
matrix = matrices.send(dimension)
next unless matrix

matrices.send("#{dimension}=", matrix_indices.map{ |r_index| matrix[r_index].values_at(*matrix_indices) })
}
}
end
Expand Down Expand Up @@ -643,26 +646,32 @@ def self.build_partial_service_vrp(service_vrp, partial_service_ids, available_v
vehicle.id == r.vehicle_id && (route_week_day.nil? || vehicle_week_day_availability.include?(route_week_day))
}
}
sub_vrp.matrices.delete_if{ |m|
sub_vrp.vehicles.none?{ |vehicle| vehicle.matrix_id == m.id }
}
end
sub_vrp.services = sub_vrp.services.select{ |service| partial_service_ids.include?(service.id) }.compact
points_ids = sub_vrp.services.map{ |s| s.activity.point.id }.uniq.compact
sub_vrp.rests = sub_vrp.rests.select{ |r| sub_vrp.vehicles.flat_map{ |v| v.rests.map(&:id) }.include? r.id }
sub_vrp.relations = sub_vrp.relations.select{ |r| r.linked_ids.all? { |id| sub_vrp.services.any? { |s| s.id == id } } }
sub_vrp.services.select!{ |service| partial_service_ids.include?(service.id) }
rest_ids = sub_vrp.vehicles.flat_map{ |v| v.rests.map(&:id) }.uniq
sub_vrp.rests.select!{ |r| rest_ids.include?(r.id) }
if entity == :vehicle
sub_vrp.relations.delete_if{ |r| r.type == :same_vehicle }
sub_vrp.services.each{ |s| s.relations.delete_if{ |r| r.type == :same_vehicle } }
end
sub_vrp.points = sub_vrp.points.select{ |p| points_ids.include? p.id }.compact
sub_vrp.points += sub_vrp.vehicles.flat_map{ |vehicle| [vehicle.start_point, vehicle.end_point] }.compact.uniq
sub_vrp.relations.select!{ |r|
# Split respects relations, it is enough to check only the first linked id -- [0..0].any? is to handle empties
r.linked_ids[0..0].any? { |sid| sub_vrp.services.any? { |s| s.id == sid } } &&
r.linked_vehicle_ids[0..0].any? { |vid| sub_vrp.vehicles.any? { |v| v.id == vid } }
}
points_ids = sub_vrp.services.map{ |s| s.activity.point.id }.compact |
sub_vrp.vehicles.flat_map{ |vehicle| [vehicle.start_point_id, vehicle.end_point_id] }.compact
sub_vrp.points.select!{ |p| points_ids.include?(p.id) }
sub_vrp.vehicles.each{ |vehicle|
vehicle.start_point = sub_vrp.points.find{ |p| p.id == vehicle.start_point_id } if vehicle.start_point
vehicle.end_point = sub_vrp.points.find{ |p| p.id == vehicle.end_point_id } if vehicle.end_point
}
sub_vrp = add_corresponding_entity_skills(entity, sub_vrp)

if !sub_vrp.matrices&.empty?
matrix_indices = sub_vrp.points.map{ |point| point.matrix_index }
update_matrix_index(sub_vrp)
update_matrix(sub_vrp.matrices, sub_vrp, matrix_indices)
update_matrix(sub_vrp, matrix_indices)
end

log "<--- build_partial_service_vrp takes #{Time.now - tic}", level: :debug
Expand Down
1 change: 1 addition & 0 deletions test/api/v01/vrp_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ def test_block_call_under_clustering
vrp1[:configuration][:preprocessing][:partitions] = TestHelper.vehicle_and_days_partitions

vrp2 = VRP.independent_skills
vrp2[:matrices][0][:distance] = Oj.load(Oj.dump(vrp2[:matrices][0][:time]))
vrp2[:points] = VRP.lat_lon_periodic[:points]
vrp2[:services].first[:skills] = ['D']
vrp2[:configuration][:preprocessing] = {
Expand Down
18 changes: 15 additions & 3 deletions test/lib/interpreters/split_clustering_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,10 @@ def test_correct_number_of_visits_when_concurrent_split_independent_and_max_spli
# and then max_split again during solution process
# should not raise "Wrong number of visits returned in result" error
vrp = VRP.independent_skills
vrp[:matrices][0][:distance] = Oj.load(Oj.dump(vrp[:matrices][0][:time]))
vrp[:points] = VRP.lat_lon_periodic[:points]
vrp[:services].first[:skills] = ['D']
vrp[:configuration][:preprocessing] = {
max_split_size: 4
}
vrp[:configuration][:preprocessing] = { max_split_size: 4 }

OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
end
Expand Down Expand Up @@ -758,6 +757,7 @@ def test_results_regularity
vrp = Marshal.dump(TestHelper.load_vrp(self)) # call load_vrp only once to not to dump for each restart
(1..@regularity_restarts).each{ |trial|
puts "Regularity trial: #{trial}/#{@regularity_restarts}"
Models.delete_all
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, Marshal.load(vrp), nil) # rubocop: disable Security/MarshalLoad
visits_unassigned << result[:unassigned].size
unassigned_service_ids = result[:unassigned].collect{ |unassigned| unassigned[:original_service_id] }
Expand Down Expand Up @@ -801,6 +801,7 @@ def test_results_regularity_2
vrp = Marshal.dump(TestHelper.load_vrp(self)) # call load_vrp only once to not to dump for each restart
(1..@regularity_restarts).each{ |trial|
OptimizerLogger.log "Regularity trial: #{trial}/#{@regularity_restarts}"
Models.delete_all
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, Marshal.load(vrp), nil) # rubocop: disable Security/MarshalLoad
visits_unassigned << result[:unassigned].size
unassigned_service_ids = result[:unassigned].collect{ |unassigned| unassigned[:original_service_id] }
Expand Down Expand Up @@ -960,6 +961,17 @@ def test_list_vehicles
assert_equal 5, Interpreters::SplitClustering.list_vehicles({ start: 0, end: 4 }, vrp.vehicles, :work_day).size
end

def test_split_keeps_matrices_in_case_vehicles_are_moved_between_subproblems
problem = VRP.lat_lon_two_vehicles
problem[:vehicles].last[:matrix_id] = 'm2'
problem[:matrices] << problem[:matrices][0].merge({ id: 'm2' })
vrp = TestHelper.create(problem)

sub_vrp = Interpreters::SplitClustering.build_partial_service_vrp({ vrp: vrp }, vrp.services.map(&:id), [0])[:vrp]

assert_equal %w[m1 m2], sub_vrp.matrices.map(&:id), 'Split should not eliminate matrices in case vehicles are moved between subproblems'
end

def test_split_with_vehicle_alternative_skills
problem = VRP.lat_lon_two_vehicles

Expand Down
3 changes: 2 additions & 1 deletion test/real_cases_periodic_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def test_minimum_stop_in_route
assert result[:routes].any?{ |r| r[:activities].size - 2 < 5 },
'We expect at least one route with less than 5 services, this test is useless otherwise'
should_remain_assigned = result[:routes].sum{ |r| r[:activities].size - 2 >= 5 ? r[:activities].size - 2 : 0 }
should_remain_unassigned = result[:unassigned].size

# one vehicle should have at least 5 stops :
vrp.vehicles.each{ |v| v.cost_fixed = 5 }
Expand All @@ -135,7 +136,7 @@ def test_minimum_stop_in_route
assert result[:routes].all?{ |r| (r[:activities].size - 2).zero? || r[:activities].size - 2 >= 5 },
'Expecting no route with less than 5 stops unless it is an empty route'
assert_operator should_remain_assigned, :<=, (result[:routes].sum{ |r| r[:activities].size - 2 })
assert_equal 25, result[:unassigned].size
assert_operator result[:unassigned].size, :>=, should_remain_unassigned

all_ids = (result[:routes].flat_map{ |route| route[:activities].collect{ |stop| stop[:service_id] } }.compact +
result[:unassigned].collect{ |un| un[:service_id] }).uniq
Expand Down
4 changes: 2 additions & 2 deletions test/wrapper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1043,9 +1043,9 @@ def test_input_zones
distance: [[0, 376184], [379177, 0]]
}],
points: [{
id: 'point_0', location: { lat: 48, lon: 5 }
id: 'point_0', location: { lat: 48, lon: 5 }, matrix_index: 0
}, {
id: 'point_1', location: { lat: 49, lon: 1 }
id: 'point_1', location: { lat: 49, lon: 1 }, matrix_index: 1
}],
zones: [{
id: 'zone_0',
Expand Down