Skip to content

Commit

Permalink
Rebase finalization
Browse files Browse the repository at this point in the history
  • Loading branch information
Braktar committed Aug 17, 2021
1 parent 1bb7c6a commit 14c80c2
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 95 deletions.
9 changes: 9 additions & 0 deletions models/solution/solution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,14 @@ def +(other)
solution.configuration = self.configuration + other.configuration
solution
end

def update_costs
previous_total = cost_info.total
# When there is only one route, the route cost_info object is shared with the solution cost_info
return if routes.size <= 1

cost_info = routes.map(&:cost_info).sum
self.cost -= (previous_total - cost_info.total).round
end
end
end
39 changes: 21 additions & 18 deletions test/lib/heuristics/periodic_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,8 @@ def test_unavailable_days_in_periodic

[[[], [0, 2, 4]], [[2], [0, 4, 6]]].each{ |unavailable_indices, expectation|
vrp[:services].first[:unavailable_visit_day_indices] = unavailable_indices
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }},
TestHelper.create(vrp), nil)
days_with_service =
solutions[0].routes.collect{ |r|
r.vehicle.id.split('_').last.to_i if r.steps.any?{ |a| a.id == 'service_1' }
Expand All @@ -721,37 +722,39 @@ def test_unavailable_days_in_periodic
end

def test_when_providing_possible_days
problem = VRP.periodic
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(Oj.load(Oj.dump(problem))), nil)
service_at_first_day_by_default = result[:routes].first[:activities].find{ |a| a[:service_id] }[:original_service_id]
vrp = VRP.periodic
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
service_at_first_day_by_default = solutions[0].routes.first.steps.find(&:service_id).id

problem[:services].find{ |s| s[:id] == service_at_first_day_by_default }[:first_possible_day_indices] = [2]
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
refute_includes result[:routes].first[:activities].collect{ |a| a[:original_service_id] }, service_at_first_day_by_default
vrp[:services].find{ |s| s[:id] == service_at_first_day_by_default }[:first_possible_day_indices] = [2]
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
refute_includes solutions[0].routes.first.steps.collect(&:id), service_at_first_day_by_default

problem[:services].each{ |s| s[:first_possible_day_indices] = [] }
problem[:services].each{ |s| s[:last_possible_day_indices] = [0] }
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal 3, (result[:routes].first[:activities].count{ |a| a[:service_id] })
vrp[:services].each{ |s|
s[:first_possible_day_indices] = []
s[:last_possible_day_indices] = [0]
}
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
assert_equal 3, solutions[0].routes.first.steps.count(&:service_id)
end

def test_first_last_possible_day_respected
vrp = VRP.periodic
vrp[:services][1][:visits_number] = 2
vrp[:services][1][:minimum_lapse] = 1
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
second_route = result[:routes].find{ |r| r[:vehicle_id] == 'vehicle_0_1' }
assert second_route[:activities].any?{ |a| a[:service_id] == 'service_2_2_2' },
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
second_route = solutions[0].routes.find{ |r| r[:vehicle_id] == 'vehicle_0_1' }
assert second_route.steps.any?{ |a| a[:service_id] == 'service_2_2_2' },
'This test is not consistent if this fails'
assert result[:routes][-1][:activities].any?{ |a| a[:service_id] == 'service_1_1_1' },
assert solutions[0].routes[-1].steps.any?{ |a| a[:service_id] == 'service_1_1_1' },
'This test is not consistent if this fails'

vrp[:services][0][:last_possible_day_indices] = [1]
vrp[:services][1][:first_possible_day_indices] = [1]
result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
assert [0, 1].any?{ |r_i| result[:routes][r_i][:activities].any?{ |a| a[:service_id] == 'service_1_1_1' } },
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
assert [0, 1].any?{ |r_i| solutions[0].routes[r_i].steps.any?{ |a| a.service_id == 'service_1_1_1' } },
'Service_1 can not take place after day index 1'
assert result[:routes][0][:activities].none?{ |a| a[:service_id] == 'service_2_1_2' },
assert solutions[0].routes[0].steps.none?{ |a| a.service_id == 'service_2_1_2' },
'Service_2 can not take before day index 1'
end
end
Expand Down
2 changes: 1 addition & 1 deletion test/real_cases_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def test_ortools_open_timewindows
"Too long travel time: #{solutions[0].routes.sum{ |r| r.info.total_travel_time }}"

# Check elapsed time
assert solutions[0].elapsed < 5000, "Too long elapsed time: #{solutions[0].elapsed}"
assert solutions[0].elapsed < 15000, "Too long elapsed time: #{solutions[0].elapsed}"
end

# Nantes - A single route with an order defining the most part of the route
Expand Down
43 changes: 22 additions & 21 deletions test/wrapper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3289,18 +3289,18 @@ def test_prioritize_first_available_trips_and_vehicles
# Solve WITH simplification and note the cost and check if trips are not skipped
vrp = TestHelper.load_vrp(self, fixture_file: 'vrp_multi_trips_which_uses_trip2_before_trip1')

soln_with_simplification = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, vrp, nil)
solns_with_simplification = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, vrp, nil)

there_is_no_skipped_trip_when_simplification_is_on = vrp.relations.none?{ |relation|
skipped_a_trip = false
relation.linked_vehicle_ids.any?{ |vid|
route = soln_with_simplification[:routes].find{ |r| r[:vehicle_id] == vid }
route = solns_with_simplification[0].routes.find{ |r| r.vehicle.id == vid }

# if an earlier trip is skipped check if a later trip is active
if skipped_a_trip
route[:end_time] > route[:start_time]
route.info.end_time > route.info.start_time
else
skipped_a_trip = (route[:end_time] <= route[:start_time])
skipped_a_trip = (route.info.end_time <= route.info.start_time)
nil
end
}
Expand All @@ -3310,19 +3310,19 @@ def test_prioritize_first_available_trips_and_vehicles
# Solve WITHOUT simplification (when prioritize_first_available_trips_and_vehicles is off)
# verify the cost is the same or better with the simplification and that the trip2 is used before trip1
function_called = false
soln_without_simplification = Wrappers::Wrapper.stub_any_instance(:prioritize_first_available_trips_and_vehicles,
proc{
function_called = true
nil
}) do
solns_without_simplification = Wrappers::Wrapper.stub_any_instance(:prioritize_first_available_trips_and_vehicles,
proc{
function_called = true
nil
}) do
vrp = TestHelper.load_vrp(self, fixture_file: 'vrp_multi_trips_which_uses_trip2_before_trip1')
OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil)
end
assert function_called, 'prioritize_first_available_trips_and_vehicles should have been called'

assert_operator soln_with_simplification[:cost], :<=, soln_without_simplification[:cost],
assert_operator solns_with_simplification[0].cost, :<=, solns_without_simplification[0].cost,
'simplification should not increase the cost'
assert_operator soln_with_simplification[:unassigned].size, :<=, soln_without_simplification[:unassigned].size,
assert_operator solns_with_simplification[0].unassigned.size, :<=, solns_without_simplification[0].unassigned.size,
'simplification should not increase unassigned services'

# If the next check starts to fail regularly, it may be removed after verification
Expand All @@ -3332,13 +3332,13 @@ def test_prioritize_first_available_trips_and_vehicles
there_is_a_skipped_trip_when_simplification_is_off = vrp.relations.any?{ |relation|
skipped_a_trip = false
relation.linked_vehicle_ids.any?{ |vid|
route = soln_without_simplification[:routes].find{ |r| r[:vehicle_id] == vid }
route = solns_without_simplification[0].routes.find{ |r| r.vehicle.id == vid }

# if an earlier trip is skipped check if a later trip is active
if skipped_a_trip
route[:end_time] > route[:start_time]
route.info.end_time > route.info.start_time
else
skipped_a_trip = (route[:end_time] <= route[:start_time])
skipped_a_trip = (route.info.end_time <= route.info.start_time)
nil
end
}
Expand All @@ -3361,23 +3361,23 @@ def test_simplify_complex_multi_pickup_or_delivery_shipments
nil
}) do
vrp = TestHelper.load_vrp(self, fixture_file: 'vrp_multipickup_singledelivery_shipments')
OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil)
OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil).first
end
assert function_called, 'simplify_complex_multi_pickup_or_delivery_shipments should have been called'
# if there are no unassigned; maybe or-tools improved its performance
# and this simplification might not be necessary anymore,
# or this instance is too small and timing needs to be fixed.
# In any case, testing with a bigger instance might be necessary if the following condition fails regularly.
refute_empty result_wo_simplify[:unassigned], 'There should have been some unassigned'
refute_empty result_wo_simplify.unassigned, 'There should have been some unassigned'

# Solve WITH simplification
vrp = TestHelper.load_vrp(self, fixture_file: 'vrp_multipickup_singledelivery_shipments')
result_w_simplify = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil)
assert_operator result_w_simplify[:unassigned].size, :<, result_wo_simplify[:unassigned].size,
sols_w_simplify = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil)
assert_operator sols_w_simplify[0].unassigned.size, :<, result_wo_simplify[:unassigned].size,
'Simplification should improve performance'
# if there are unassigned services; this might be due to computer performance
# but normally even 0.5 seconds is enough, so it might be due to a change in or-tools or optimizer-ortools
assert_empty result_w_simplify[:unassigned], 'Simplification should plan all services'
assert_empty sols_w_simplify[0].unassigned, 'Simplification should plan all services'
end

def test_reject_when_unfeasible_timewindows
Expand All @@ -3395,8 +3395,9 @@ def test_multiple_reason
problem[:vehicles].first[:timewindow] = { start: 0, end: 24500 }
problem[:vehicles].first[:capacities] = [{ unit_id: 'kg', limit: 1100 }]

result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:demo] }}, TestHelper.load_vrp(self, problem: problem), nil)
assert(result[:unassigned].collect{ |una| una[:reason].include?('&&') })
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:demo] }},
TestHelper.load_vrp(self, problem: problem), nil)
assert(solutions[0].unassigned.collect{ |una| una.reason.include?('&&') })
end

def test_possible_days_consistency
Expand Down
18 changes: 8 additions & 10 deletions test/wrappers/ortools_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4161,7 +4161,7 @@ def test_evaluate_only_not_every_service_has_route
solution = ortools.solve(vrp, 'test')
assert solution
assert_equal 1, solution.unassigned.size
assert_equal 1, solution.cost_details.total # exclusion costs are not included in the cost_details
assert_equal 1, solution.cost_info.total # exclusion costs are not included in the cost_details
assert_equal 1, solution.iterations
end

Expand Down Expand Up @@ -4863,7 +4863,7 @@ def test_subproblem_with_one_vehicle_and_no_possible_service
}
vrp = TestHelper.create(problem)
solutions = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, vrp, nil)
assert_equal 0, solutions[0].routes[0].activities.size, 'All services should be eliminated'
assert_equal 0, solutions[0].routes[0].steps.size, 'All services should be eliminated'
assert_equal 2, solutions[0].unassigned.size, 'All services should be eliminated'
assert_equal 0, solutions[0].cost, 'All eliminated, cost should be 0'
end
Expand Down Expand Up @@ -4970,16 +4970,14 @@ def test_minimum_duration_lapse_shipments
shipment0_route.steps[pickup0_index].info.begin_time
end

def test_cost_details
def test_cost_info
vrp = VRP.basic
vrp[:units] = [{ id: 'kg' }]
vrp[:vehicles].first.merge!(cost_fixed: 1, cost_time_multiplier: 2,
capacities: [{ unit_id: 'kg', limit: 1, overload_multiplier: 0.3 }])
vrp[:services].first[:quantities] = [{ unit_id: 'kg', value: 2 }]
vrp[:services].first[:activity].merge!(timewindows: [{ start: 0, end: 1 }], late_multiplier: 0.007)
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
refute (solutions[0].cost_info.object_id equal? solutions[0].routes[0].cost_info.object_id),
'cost_info object should not be the same for route and solution'
assert_equal 21.321, solutions[0].cost_info.total.round(3)
assert_equal 1, solutions[0].cost_info.fixed
assert_equal 20, solutions[0].cost_info.time
Expand Down Expand Up @@ -5293,18 +5291,18 @@ def test_respect_timewindows_without_end
problem[:services].last[:activity][:timewindows] << { start: 20 }

vrp = TestHelper.create(problem)
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, vrp, nil)
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, vrp, nil)

assert_empty result[:unassigned], 'All three services should be planned. There is an obvious feasible solution.'
assert_empty solutions[0].unassigned, 'All three services should be planned. There is an obvious feasible solution.'

vrp.services.each{ |service|
planned_begin_time = result[:routes][0][:activities].find{ |a| a[:service_id] == service.id }[:begin_time]
planned_begin_time = solutions[0].routes[0].steps.find{ |s| s.service_id == service.id }.info.begin_time
assert service.activity.timewindows.one?{ |tw|
planned_begin_time >= tw.start && (tw.end.nil? || planned_begin_time <= tw.end)
}, 'Services should respect the TW without end and fall within exactly one of its TW ranges'
}

assert_equal 20, result[:routes][0][:activities].last[:begin_time], 'Third service should be planned at 20'
assert_equal 20, solutions[0].routes[0].steps.last.info.begin_time, 'Third service should be planned at 20'
end

def test_relations_sent_to_ortools_when_different_lapses
Expand Down Expand Up @@ -5336,7 +5334,7 @@ def test_relations_sent_to_ortools_when_different_lapses
# check number of relations sent to ortools
assert_equal expected_number_of_relations[pb_index], ortools_problem.relations.size

'Job killed' # Return "Job killed" to stop gracefully
Models::Solution.new(status: :killed)
}
) do
OptimizerWrapper.solve(service: :ortools, vrp: TestHelper.create(problem.dup))
Expand Down
Loading

0 comments on commit 14c80c2

Please sign in to comment.