Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
senhalil committed Mar 1, 2021
1 parent 93231a6 commit 53e86ae
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 10 deletions.
29 changes: 19 additions & 10 deletions lib/interpreters/split_clustering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def self.split_solve_candidate?(service_vrp)
vrp.preprocessing_max_split_size &&
vrp.vehicles.size > 1 &&
(vrp.resolution_vehicle_limit.nil? || vrp.resolution_vehicle_limit > 1) &&
(vrp.shipments.size + vrp.services.size - empties_or_fills.size) > vrp.preprocessing_max_split_size &&
(vrp.shipments.size + vrp.services.size - vrp.relations.count{ |r| r.type == 'shipment' } - empties_or_fills.size) > vrp.preprocessing_max_split_size &&
vrp.shipments.all?{ |s| depot_ids.include?(s.pickup.point_id) || depot_ids.include?(s.delivery.point_id) }
else
ss_data = service_vrp[:split_solve_data]
Expand All @@ -139,7 +139,12 @@ def self.split_solve_candidate?(service_vrp)

current_vehicles.size > 1 &&
(ss_data[:current_vehicle_limit].nil? || ss_data[:current_vehicle_limit] > 1) &&
current_vehicles.sum{ |v| service_vehicle_assignments[v.id].size } > vrp.preprocessing_max_split_size
current_vehicles.sum{ |vehicle|
service_vehicle_assignments[vehicle.id].size -
service_vehicle_assignments[vehicle.id].count{ |service|
service.relations.any?{ |r| r.type == 'shipment' }
}
} > vrp.preprocessing_max_split_size
end
end

Expand Down Expand Up @@ -190,7 +195,9 @@ def self.split_solve(service_vrp, job = nil, &block)
# using the service_vehicle_assignments information
# (don't generate any sub-VRP yet)
split_solve_core(service_vrp, job, &block) # self-recursive method
ensure
rescue StandardError
raise
else
service_vrp[:vrp] = service_vrp[:split_solve_data][:original_vrp]
service_vrp[:vrp].services.concat empties_or_fills
log '<-- split_solve (clustering by max_split)'
Expand Down Expand Up @@ -908,26 +915,25 @@ def collect_data_items_metrics(vrp, cumulated_metrics, options)
}
end

# TODO: 0- this part needs to be able to handle real shipments
custom_shipments = build_services_from_shipments(depot_ids, vrp.shipments)
binding_relations = %w[same_route order sequence shipment]

(vrp.services + custom_shipments).group_by{ |s|
vrp.services.group_by{ |s|
location =
if s.activity
s.activity.point.location
elsif s.activities.size.positive?
raise UnsupportedProblemError, 'Clustering is not supported yet if one service has serveral activities.'
raise UnsupportedProblemError, 'Clustering does not support services with multiple activities.'
end

# TODO: 0- this part needs to be able to handle shipment as a relation
# TODO: 0- this part needs to be able to handle shipment as a relation and not merge the services with binding relations
{
lat: location.lat.round_with_steps(decimal[:digits], decimal[:steps]),
lon: location.lon.round_with_steps(decimal[:digits], decimal[:steps]),
v_id: s[:sticky_vehicle_ids].to_a |
[vrp.routes.find{ |r| r.mission_ids.include? s.id }&.vehicle_id].compact,
skills: s.skills.to_a.dup,
day_skills: compute_day_skills(s.activity&.timewindows),
id: options[:group_points] ? nil : s.id
id: options[:group_points] && s.relations.all?{ |r| binding_relations.exclude?(r.type) } ? nil : s.id, # use IDs to prevent grouping
}
}.each_with_index{ |(characteristics, sub_set), sub_set_index|
unit_quantities = Hash.new(0)
Expand Down Expand Up @@ -979,7 +985,10 @@ def zip_dataitems(vrp, items, grouped_objects)

c.distance_function = lambda do |data_item_a, data_item_b|
# If there is no vehicle that can serve both points at the same time, make sure they are not merged
return max_distance + 1 if (compatible_vehicles[data_item_a[2]] & compatible_vehicles[data_item_b[2]]).empty?
if data_item_a[4][:relations]&.any? || data_item_b[4][:relations]&.any? ||
(compatible_vehicles[data_item_a[2]] & compatible_vehicles[data_item_b[2]]).empty?
return max_distance + 1
end

[
vrp.matrices[0][:distance][data_item_a[4][:matrix_index]][data_item_b[4][:matrix_index]],
Expand Down
79 changes: 79 additions & 0 deletions test/lib/interpreters/split_clustering_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,85 @@ def test_max_split_can_handle_empty_vehicles
end
assert called, 'split_solve_core should have been called'
end
# focus
def test_collect_data_items_respects_binding_relations
# create all types of binding relation, all at the same location, and check if they are merged
binding_relations = %w[same_route order sequence shipment]

problem = VRP.lat_lon
dummy_service = problem[:services].first
problem[:services] = []
problem[:relations] = []
binding_relations.each{ |relation|
problem[:relations] << { type: relation, linked_ids: [] }
2.times.each{ |i|
problem[:services] << Oj.load(Oj.dump(dummy_service))
problem[:services].last[:id] = "service_#{relation}_#{i}"
problem[:relations].last[:linked_ids] << problem[:services].last[:id]
}
}

vrp = TestHelper.create(problem)
# byebug
data_items, = Interpreters::SplitClustering.send(:collect_data_items_metrics, vrp, Hash.new(0), { group_points: true, basic_split: false })
# byebug
assert_equal 7, data_items.size
end

def test_max_split_can_handle_pud_and_same_route_relations
# Services 1 and 2 are at the same location, as Services 4 and 5. And Services 3 and 6 are far away from tjh
# With Shipments s1{p:1, d:3}, s2{p:2, d:5}, s3{p:4, d:6}, we test that split does the "right" thing; even if,
# it is the "hardest" -- i.e., creating a "costly" split and forcing close points to be on different vehicles.
problem = VRP.lat_lon
4.times.each{ |i| # have enough vehicles to see if the relations are respected
problem[:vehicles] << problem[:vehicles].first.dup
problem[:vehicles].last[:id] = "vehicle_#{i + 1}"
}

# problem[:shipments] = [{ # Shipments are only supported as relations at the moment
# id: 'shipment_0',
# pickup: {
# point_id: 'point_1',
# duration: 3,
# late_multiplier: 0,
# },
# delivery: {
# point_id: 'point_3',
# duration: 3,
# late_multiplier: 0,
# }
# }]

problem[:relations] = [
{ type: 'shipment', linked_ids: [1, 3].collect{ |i| "service_#{i}" } },
{ type: 'shipment', linked_ids: [2, 5].collect{ |i| "service_#{i}" } },
{ type: 'same_route', linked_ids: [4, 6].collect{ |i| "service_#{i}" } },
# { type: 'same_route', linked_ids: [1, 4].collect{ |i| "service_#{i}" } }
]

# the split should stop even if there are "two services" (which is actually one shipment)
problem[:configuration][:preprocessing][:max_split_size] = 1

called = false
Interpreters::SplitClustering.stub(:split_solve_core, lambda{ |service_vrp, _job|
assert_operator service_vrp[:split_level], :<, 3, "split_level shouldn't reach 3. Split candidate should be able to handle binding relations!"
called = true
Interpreters::SplitClustering.send(:__minitest_stub__split_solve_core, service_vrp) # call original function
}) do
OptimizerWrapper.stub(:solve, lambda{ |service_vrp, _job, _block| # stub with empty solution
vrp = service_vrp[:vrp]
# check that only necessary relations are present with all its services
assert_empty vrp.relations.flat_map(&:linked_ids) - vrp.services.collect(&:id), 'Split does not respect relations: Missing binding services or extraneous relations'
service = service_vrp[:service]
OptimizerWrapper.config[:services][service].detect_unfeasible_services(vrp)
OptimizerWrapper.config[:services][service].empty_result(service.to_s, vrp)
}) do
vrp = TestHelper.create(problem)
Interpreters::SplitClustering.split_solve({ service: :ortools, vrp: vrp, dicho_level: 0 })
end
end
assert called, 'split_solve_core should have been called'
end

def test_ignore_debug_parameter_if_no_coordinates
vrp = TestHelper.load_vrp(self)
Expand Down

0 comments on commit 53e86ae

Please sign in to comment.