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 Jan 20, 2022
1 parent a5259bf commit b6619c5
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.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 @@ -52,6 +52,8 @@ class Service < Base
field :skills, default: []
field :original_skills, default: []

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 @@ -189,10 +189,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 @@ -1464,7 +1464,7 @@ def test_impossible_service_too_far_time
}
}
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal(1, result[:unassigned].count{ |un| un[:reason] == 'No compatible vehicle can reach this service while respecting all constraints' })
assert_equal(1, result[:unassigned].count{ |un| un[:reason]&.split(' && ')&.include?('No compatible vehicle can reach this service while respecting all constraints') })
end

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

def test_impossible_service_capacity
Expand Down Expand Up @@ -1593,7 +1593,7 @@ def test_impossible_service_capacity
}
}
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal 1, (result[:unassigned].count{ |un| un[:reason] == 'Service quantity greater than any vehicle capacity' })
assert_equal 1, (result[: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 @@ -1694,7 +1694,7 @@ def test_impossible_service_tw
}
}
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal(1, result[:unassigned].count{ |un| un[:reason].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_duration
Expand Down Expand Up @@ -1745,7 +1745,7 @@ def test_impossible_service_duration
}
}
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal 1, (result[:unassigned].count{ |un| un[:reason].include?('Service duration greater than any vehicle 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_duration_with_sequence_tw
Expand Down Expand Up @@ -1802,7 +1802,7 @@ def test_impossible_service_duration_with_sequence_tw
}
}
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(problem), nil)
assert_equal 1, (result[:unassigned].count{ |un| un[:reason].include?('Service duration greater than any vehicle 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_duration_with_two_vehicles
Expand Down Expand Up @@ -1869,7 +1869,7 @@ def test_impossible_service_tw_periodic
]
vrp[:services].first[:activity][:timewindows] = [{ start: 0, end: 5, day_index: 1 }]
result = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil)
assert_equal(1, result[:unassigned].count{ |un| un[:reason] == '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_due_to_unavailable_day_periodic
Expand All @@ -1882,7 +1882,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 @@ -2526,7 +2526,7 @@ def test_impossible_service_too_long
}
vrp[:services].first[:activity][:duration] = 15
result = OptimizerWrapper.config[:services][:demo].detect_unfeasible_services(TestHelper.create(vrp))
assert_equal(1, result.values.flatten.count{ |un| un[:reason] == 'Service duration greater than any vehicle timewindow' })
assert_equal(1, result.values.flatten.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 @@ -2536,7 +2536,7 @@ def test_impossible_service_too_long
vrp[:services].first[:activity][:duration] = 15
vrp[:configuration][:schedule] = { range_indices: { start: 0, end: 3 }}
result = OptimizerWrapper.config[:services][:demo].detect_unfeasible_services(TestHelper.create(vrp))
assert_equal(1, result.values.flatten.count{ |un| un[:reason] == 'Service duration greater than any vehicle timewindow' })
assert_equal(1, result.values.flatten.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 @@ -2553,7 +2553,7 @@ def test_impossible_service_with_negative_quantity

vrp[:services].first[:quantities].first[:value] = -6
result = OptimizerWrapper.config[:services][:demo].detect_unfeasible_services(TestHelper.create(vrp))
assert_equal(1, result.values.flatten.count{ |un| un[:reason] == 'Service quantity greater than any vehicle capacity' })
assert_equal(1, result.values.flatten.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 @@ -2839,9 +2839,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 @@ -3503,7 +3503,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))
assert_equal 1, (unfeasible_services.values.flatten.count{ |un| un[:reason] == 'Service timewindows are infeasible' })
assert_equal 1, (unfeasible_services.values.flatten.count{ |un| un[:reason].split(' && ').include?('Service timewindow is 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 @@ -2740,12 +2740,12 @@ def test_shipments
vrp = TestHelper.create(VRP.pud)
result = ortools.solve(vrp, 'test')
assert result
assert_equal 0, result[:unassigned].size
assert_equal 6, result[:routes][0][:activities].size
assert result[:routes][0][:activities].index{ |activity| activity[:pickup_shipment_id] == 'shipment_0' } <
result[:routes][0][:activities].index{ |activity| activity[:delivery_shipment_id] == 'shipment_0' }
assert result[:routes][0][:activities].index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } <
result[:routes][0][:activities].index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
assert_equal 0, result[:unassigned].size
assert_equal 6, result[:routes][0][:activities].size
end

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

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

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

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

def test_shipments_distance
Expand All @@ -2987,12 +2987,12 @@ def test_shipments_distance
vrp = TestHelper.create(problem)
result = ortools.solve(vrp, 'test')
assert result
assert_equal 0, result[:unassigned].size
assert_equal 6, result[:routes][0][:activities].size
assert result[:routes][0][:activities].index{ |activity| activity[:pickup_shipment_id] == 'shipment_0' } <
result[:routes][0][:activities].index{ |activity| activity[:delivery_shipment_id] == 'shipment_0' }
assert result[:routes][0][:activities].index{ |activity| activity[:pickup_shipment_id] == 'shipment_1' } <
result[:routes][0][:activities].index{ |activity| activity[:delivery_shipment_id] == 'shipment_1' }
assert_equal 0, result[:unassigned].size
assert_equal 6, result[:routes][0][:activities].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 @@ -139,19 +139,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 @@ -350,8 +346,13 @@ def build_cost_details(cost_details)
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(vehicle_matrix, previous_matrix_index, current_matrix_index)
Expand Down
Loading

0 comments on commit b6619c5

Please sign in to comment.