Skip to content

Commit

Permalink
Add: keep a list of service-vehicle compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
senhalil committed Feb 3, 2022
1 parent e392dce commit 8ee3ada
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 188 deletions.
2 changes: 2 additions & 0 deletions models/concerns/periodic_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ def can_affect_all_visits?(service)
return true if service.visits_number == 1

self.vehicles.any?{ |vehicle|
next unless service.vehicle_compatibility.nil? || service.vehicle_compatibility[vehicle.id] # vehicle already eliminated

current_day = self.configuration.schedule.range_indices[:start]
decimal_day = current_day
current_visit = 0
Expand Down
2 changes: 2 additions & 0 deletions models/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class Service < Base
field :skills
field :original_skills

field :vehicle_compatibility # vehicle_compatibility[v_id] == {true -> compatible, false -> incompatible, nil -> not checked yet}

## has_many :period_activities, class_name: 'Models::Activity' # Need alternatives visits
belongs_to :activity, class_name: 'Models::Activity'
has_many :activities, class_name: 'Models::Activity'
Expand Down
10 changes: 10 additions & 0 deletions models/timewindow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,15 @@ def self.create(hash)

super(hash)
end

def safe_end(lateness_allowed = false)
if self.end
self.end + (lateness_allowed ? self.maximum_lateness : 0)
elsif self.day_index
86399 # 24h - 1sec
else
2147483647 # 2**31 - 1
end
end
end
end
4 changes: 4 additions & 0 deletions optimizer_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,14 @@ def self.solve(service_vrp, job = nil, block = nil)
else
unfeasible_services = config[:services][service].detect_unfeasible_services(vrp)

# TODO: Eliminate the points which has no feasible vehicle or service

vrp.compute_matrix(&block)

config[:services][service].check_distances(vrp, unfeasible_services)

# TODO: Eliminate the vehicles which cannot serve any service vrp.services.all?{ |s| s.vehicle_compatibility[v.id] == false }

# Remove infeasible services
services_to_reinject = []
unfeasible_services.each_key{ |una_service_id|
Expand Down
28 changes: 14 additions & 14 deletions test/wrapper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1470,7 +1470,7 @@ def test_impossible_service_too_far_time
}
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal(1, solutions[0].unassigned.count{ |un|
un.reason == 'No compatible vehicle can reach this service while respecting all constraints'
un.reason&.split(' && ')&.include?('No compatible vehicle can reach this service while respecting all constraints')
})
end

Expand Down Expand Up @@ -1531,7 +1531,7 @@ def test_impossible_service_too_far_distance
}
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal(1, solutions[0].unassigned.count{ |un|
un.reason == 'No compatible vehicle can reach this service while respecting all constraints'
un.reason&.split(' && ')&.include?('No compatible vehicle can reach this service while respecting all constraints')
})
end

Expand Down Expand Up @@ -1602,7 +1602,7 @@ def test_impossible_service_capacity
}
}
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal 1, (solutions[0].unassigned.count{ |un| un.reason == 'Service quantity greater than any vehicle capacity' })
assert_equal 1, (solutions[0].unassigned.count{ |un| un.reason&.split(' && ')&.include?('Service has a quantity which is greater than the capacity of any compatible vehicle') })
end

def test_impossible_service_skills
Expand Down Expand Up @@ -1703,7 +1703,7 @@ def test_impossible_service_tw
}
}
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal(1, solutions[0].unassigned.count{ |un| un.reason.include?('No vehicle with compatible timewindow') })
assert_equal(1, solutions[0].unassigned.count{ |un| un.reason.include?('Service cannot be performed by any compatible vehicle while respecting duration, timewindow and day limits') })
end

def test_impossible_service_duration
Expand Down Expand Up @@ -1755,7 +1755,7 @@ def test_impossible_service_duration
}
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal 1, (solutions[0].unassigned.count{ |un|
un.reason&.include?('Service duration greater than any vehicle timewindow')
un.reason&.include?('Service cannot be performed by any compatible vehicle while respecting duration, timewindow and day limits')
})
end

Expand Down Expand Up @@ -1814,7 +1814,7 @@ def test_impossible_service_duration_with_sequence_tw
}
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal 1, (solutions[0].unassigned.count{ |un|
un.reason.include?('Service duration greater than any vehicle timewindow')
un.reason.include?('Service cannot be performed by any compatible vehicle while respecting duration, timewindow and day limits')
})
end

Expand Down Expand Up @@ -1882,7 +1882,7 @@ def test_impossible_service_tw_periodic
]
vrp[:services].first[:activity][:timewindows] = [{ start: 0, end: 5, day_index: 1 }]
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
assert_equal(1, solutions[0].unassigned.count{ |un| un.reason == 'No vehicle with compatible timewindow' })
assert_equal(1, solutions[0].unassigned.count{ |un| un.reason&.split(' && ')&.include?('Service cannot be performed by any compatible vehicle while respecting duration, timewindow and day limits') })
end

def test_impossible_service_due_to_unavailable_day_periodic
Expand All @@ -1895,7 +1895,7 @@ def test_impossible_service_due_to_unavailable_day_periodic
vrp[:services].first[:activity][:timewindows] = [{ start: 0, end: 5, day_index: 0 }]
vrp[:services].first[:unavailable_visit_day_indices] = [0]
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
assert_equal(1, result[:unassigned].count{ |un| un[:reason].split(' && ').include?('No vehicle with compatible timewindow') })
assert_equal(1, result[:unassigned].count{ |un| un[:reason].split(' && ').include?('Service cannot be performed by any compatible vehicle while respecting duration, timewindow and day limits') })
end

def test_impossible_service_distance
Expand Down Expand Up @@ -2549,7 +2549,7 @@ def test_impossible_service_too_long
}
vrp[:services].first[:activity][:duration] = 15
unfeasible = OptimizerWrapper.config[:services][:demo].detect_unfeasible_services(TestHelper.create(vrp)).values.flatten
assert_equal(1, unfeasible.count{ |un| un.reason == 'Service duration greater than any vehicle timewindow' })
assert_equal(1, unfeasible.count{ |un| un.reason == 'Service cannot be performed by any compatible vehicle while respecting duration, timewindow and day limits' })

vrp[:vehicles].first[:timewindow] = nil
vrp[:vehicles].first[:sequence_timewindows] = [{
Expand All @@ -2559,7 +2559,7 @@ def test_impossible_service_too_long
vrp[:services].first[:activity][:duration] = 15
vrp[:configuration][:schedule] = { range_indices: { start: 0, end: 3 }}
unfeasible = OptimizerWrapper.config[:services][:demo].detect_unfeasible_services(TestHelper.create(vrp)).values.flatten
assert_equal(1, unfeasible.count{ |un| un.reason == 'Service duration greater than any vehicle timewindow' })
assert_equal(1, unfeasible.count{ |un| un.reason == 'Service cannot be performed by any compatible vehicle while respecting duration, timewindow and day limits' })

vrp[:vehicles].first[:sequence_timewindows] << {
start: 0,
Expand All @@ -2576,7 +2576,7 @@ def test_impossible_service_with_negative_quantity

vrp[:services].first[:quantities].first[:value] = -6
unfeasible = OptimizerWrapper.config[:services][:demo].detect_unfeasible_services(TestHelper.create(vrp)).values.flatten
assert_equal(1, unfeasible.count{ |un| un.reason == 'Service quantity greater than any vehicle capacity' })
assert_equal(1, unfeasible.count{ |un| un.reason&.split(' && ')&.include?('Service has a quantity which is greater than the capacity of any compatible vehicle') })
end

def test_feasible_if_tardiness_allowed
Expand Down Expand Up @@ -2869,9 +2869,9 @@ def test_detecting_unfeasible_services_can_not_take_too_long
# corrected/increased. On local, the total_times are almost half the limits.
# The goal of this test is to prevent adding an involuntary exponential logic and the limits can increase linearly
# if more verifications are added but the time should not jump orders of magnitude.
assert_operator total_check_distances_time, :<=, 3.3, 'check_distances function took longer than expected'
assert_operator total_check_distances_time, :<=, 6.6, 'check_distances function took longer than expected'
assert_operator total_add_unassigned_time, :<=, 6.6, 'add_unassigned function took longer than expected'
assert_operator total_detect_unfeasible_services_time, :<=, 8.8, 'detect_unfeasible_services function took too long'
assert_operator total_detect_unfeasible_services_time, :<=, 14.5, 'detect_unfeasible_services function took too long'
ensure
OptimizerLogger.level = old_logger_level if old_logger_level
OptimizerWrapper.config[:solve][:repetition] = old_config_solve_repetition if old_config_solve_repetition
Expand Down Expand Up @@ -3563,7 +3563,7 @@ def test_reject_when_unfeasible_timewindows
vrp[:services].first[:activity][:timewindows] = [{ start: 0, end: 10 }, { start: 20, end: 15 }]
# ship and service but only check service
unfeasible_services = OptimizerWrapper.config[:services][:demo].detect_unfeasible_services(TestHelper.create(vrp)).values.flatten
assert_equal 1, (unfeasible_services.count{ |un| un.reason == 'Service timewindows are infeasible' })
assert_equal 1, (unfeasible_services.count{ |un| un.reason&.split(' && ')&.include?('Service timewindows are infeasible') })
end

def test_multiple_reason
Expand Down
24 changes: 12 additions & 12 deletions test/wrappers/ortools_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2734,12 +2734,12 @@ def test_shipments
vrp = TestHelper.create(VRP.pud)
solution = ortools.solve(vrp, 'test')
assert solution
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes.first.stops.size
assert solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_0' } <
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_0' }
assert solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } <
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes.first.stops.size
end

def test_shipments_quantities
Expand Down Expand Up @@ -2825,12 +2825,12 @@ def test_shipments_quantities
vrp = TestHelper.create(problem)
solution = ortools.solve(vrp, 'test')
assert solution
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes.first.stops.size
assert_equal(solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_0' } + 1,
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_0' })
assert_equal(solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } + 1,
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' })
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes.first.stops.size
end

def test_shipments_with_multiple_timewindows_and_lateness
Expand All @@ -2850,12 +2850,12 @@ def test_shipments_with_multiple_timewindows_and_lateness
vrp = TestHelper.create(problem)
solution = ortools.solve(vrp, 'test')
assert_equal 0, solution.cost_info.lateness
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes[0].stops.size
assert solution.routes[0].stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_0' } <
solution.routes[0].stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_0' }
assert solution.routes[0].stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } <
solution.routes[0].stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes[0].stops.size
end

def test_shipments_inroute_duration
Expand All @@ -2866,6 +2866,8 @@ def test_shipments_inroute_duration
vrp = TestHelper.create(problem)
solution = ortools.solve(vrp, 'test')
assert solution
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes[0].stops.size
assert_equal(
solution.routes.first.stops.find_index{ |activity| activity[:pickup_shipment_id] == 'shipment_0' } + 1,
solution.routes.first.stops.find_index{ |activity| activity[:delivery_shipment_id] == 'shipment_0' }
Expand All @@ -2884,8 +2886,6 @@ def test_shipments_inroute_duration
:<,
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
)
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes.first.stops.size
end

def test_mixed_shipments_and_services
Expand Down Expand Up @@ -2961,10 +2961,10 @@ def test_mixed_shipments_and_services
vrp = TestHelper.create(problem)
solution = ortools.solve(vrp, 'test')
assert solution
assert solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } <
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
assert_equal 0, solution.unassigned.size
assert_equal 5, solution.routes.first.stops.size
assert solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } <
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
end

def test_shipments_distance
Expand All @@ -2981,12 +2981,12 @@ def test_shipments_distance
vrp = TestHelper.create(problem)
solution = ortools.solve(vrp, 'test')
assert solution
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes.first.stops.size
assert solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_0' } <
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_0' }
assert solution.routes.first.stops.index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } <
solution.routes.first.stops.index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
assert_equal 0, solution.unassigned.size
assert_equal 6, solution.routes.first.stops.size
end

def test_maximum_duration_lapse_shipments
Expand Down
31 changes: 16 additions & 15 deletions wrappers/ortools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,15 @@ def solve(vrp, job, thread_proc = nil, &block)
routes = []
services_positions = { always_first: [], always_last: [], never_first: [], never_last: [] }
vrp.services.each_with_index{ |service, service_index|
vehicles_indices =
if service.skills.any? && vrp.vehicles.all?{ |vehicle| vehicle.skills.empty? } &&
service.unavailable_days.empty?
[]
else
vrp.vehicles.collect.with_index{ |vehicle, index|
if (service.skills.empty? || vehicle.skills.any?{ |skill_set| (service.skills - skill_set).empty? }) &&
check_services_compatible_days(vrp, vehicle, service) &&
(service.unavailable_days.empty? || !service.unavailable_days.include?(vehicle.global_day_index))
index
end
}.compact
end
vehicles_indices = []
detect_unfeasible_services(vrp) if service.vehicle_compatibility.nil?
vrp.vehicles.each_with_index{ |vehicle, index|
next unless (service.vehicle_compatibility[vehicle.id].nil? || service.vehicle_compatibility[vehicle.id]) &&
service.vehicle_compatibility[vehicle.original_id] &&
check_services_compatible_days(vrp, vehicle, service)

vehicles_indices << index
}

if service.activity
services << OrtoolsVrp::Service.new(
Expand Down Expand Up @@ -430,8 +426,13 @@ def build_solution(vrp, content)
end

def check_services_compatible_days(vrp, vehicle, service)
!(vrp.schedule? && (service.minimum_lapse || service.maximum_lapse)) ||
vehicle.global_day_index.between?(service.first_possible_days.first, service.last_possible_days.first)
!vrp.schedule? || (
service.unavailable_days.exclude?(vehicle.global_day_index) &&
(
(!service.minimum_lapse && !service.maximum_lapse) ||
vehicle.global_day_index.between?(service.first_possible_days.first, service.last_possible_days.first)
)
)
end

def build_route_data(stop, vehicle_matrix, previous_matrix_index, current_matrix_index)
Expand Down
Loading

0 comments on commit 8ee3ada

Please sign in to comment.