Skip to content

Commit

Permalink
Ensure checks are done on every visit when needed
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsecadeline committed Jun 11, 2021
1 parent bbc4997 commit 9275236
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 99 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

### Fixed

- Capacity violation from periodic heuristic [#227](https://github.com/Mapotempo/optimizer-api/pull/227)

## [v1.7.1] - 2021-05-20

### Added
Expand Down
2 changes: 1 addition & 1 deletion lib/heuristics/concerns/scheduling_data_initialisation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def initialize_routes(routes)
defined_route.mission_ids.each{ |id|
next if !@services_data.has_key?(id) # id has been removed when detecting unfeasible services in wrapper

best_index = find_best_index(id, associated_route) if associated_route
best_index = find_best_index(id, associated_route, false) if associated_route
considered_ids << id
if best_index
insert_point_in_route(associated_route, best_index, false)
Expand Down
69 changes: 22 additions & 47 deletions lib/heuristics/concerns/scheduling_end_phase.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ def refine_solution(&block)
end
end

def days_respecting_lapse(id, vehicle_id)
def days_respecting_lapse(id, vehicle_routes)
min_lapse = @services_data[id][:raw].minimum_lapse
max_lapse = @services_data[id][:raw].maximum_lapse
used_days = @services_data[id][:used_days]

return @candidate_routes[vehicle_id].keys if used_days.empty?
return vehicle_routes.keys if used_days.empty?

@candidate_routes[vehicle_id].keys.select{ |day|
vehicle_routes.keys.select{ |day|
smaller_lapse_with_other_days = used_days.collect{ |used_day| (used_day - day).abs }.min
(min_lapse.nil? || smaller_lapse_with_other_days >= min_lapse) &&
(max_lapse.nil? || smaller_lapse_with_other_days <= max_lapse)
Expand Down Expand Up @@ -100,15 +100,15 @@ def add_missing_visits
until costs.empty?
# select best visit to insert
max_priority = costs.keys.collect{ |id| @services_data[id][:raw].priority + 1 }.max
best_cost = costs.min_by{ |id, info| ((@services_data[id][:raw].priority.to_f + 1) / max_priority) * (info[:cost][:additional_route_time] / @services_data[id][:raw].visits_number**2) }
best_cost = costs.min_by{ |id, info| ((@services_data[id][:raw].priority.to_f + 1) / max_priority) * (info[:additional_route_time] / @services_data[id][:raw].visits_number**2) }

id = best_cost[0]
day = best_cost[1][:day]
vehicle_id = best_cost[1][:vehicle]
log "It is interesting to add #{id} at day #{day} on #{vehicle_id}", level: :debug

@ids_to_renumber |= [id]
insert_point_in_route(@candidate_routes[vehicle_id][day], best_cost[1][:cost])
insert_point_in_route(@candidate_routes[vehicle_id][day], best_cost[1])
@output_tool&.add_single_visit(day, @services_data[id][:used_days], id, @services_data[id][:raw].visits_number)

costs = update_costs(costs, best_cost)
Expand All @@ -120,15 +120,14 @@ def add_missing_visits
def update_costs(costs, best_cost)
# update costs for inserted id, available_days changed
uninserted_set = @uninserted.select{ |_key, info| info[:original_id] == best_cost[0] }.keys
@uninserted.delete(uninserted_set.first)
@uninserted.delete(uninserted_set.first) # faire en fct nb in seq
if uninserted_set.size > 1
available_days = days_respecting_lapse(best_cost[0], best_cost[1][:vehicle])
available_days = days_respecting_lapse(best_cost[0], @candidate_routes[best_cost[1][:vehicle]])

day, cost = find_best_day_cost(available_days, @candidate_routes[best_cost[1][:vehicle]], best_cost[0])
cost = find_best_day_cost(@candidate_routes[best_cost[1][:vehicle]], best_cost[0], available_days)

if cost
costs[best_cost[0]][:day] = day
costs[best_cost[0]][:cost] = cost
costs[best_cost[0]] = cost
else
costs.delete(best_cost[0])
end
Expand All @@ -140,14 +139,10 @@ def update_costs(costs, best_cost)
costs.each{ |id, info|
next if info[:day] != best_cost[1][:day] && info[:vehicle] != best_cost[1][:vehicle]

day, cost = find_best_cost(id, info[:vehicle])
cost = find_best_day_cost(@candidate_routes[info[:vehicle]], id)

if cost
costs[id] = {
day: day,
vehicle: info[:vehicle],
cost: cost
}
costs[id] = cost
else
costs.delete(id)
end
Expand All @@ -156,48 +151,28 @@ def update_costs(costs, best_cost)
costs
end

def find_best_day_cost(available_days, vehicle_routes, id)
return [nil, nil] if available_days.empty?
def find_best_day_cost(vehicle_routes, id, available_days = nil)
available_days ||= days_respecting_lapse(id, vehicle_routes)

day = available_days[0]
if @same_point_day && @services_data[id][:group_capacity].all?{ |need, quantity| quantity <= vehicle_routes[day][:capacity_left][need] } ||
!@same_point_day && @services_data[id][:capacity].all?{ |need, quantity| quantity <= vehicle_routes[day][:capacity_left][need] }
cost = find_best_index(id, vehicle_routes[day])
end
return [nil, nil] unless available_days.any?

index = 1
index = 0
cost = nil
while cost.nil? && index < available_days.size
day = available_days[index]

if @same_point_day && @services_data[id][:group_capacity].all?{ |need, quantity| quantity <= vehicle_routes[day][:capacity_left][need] } ||
!@same_point_day && @services_data[id][:capacity].all?{ |need, quantity| quantity <= vehicle_routes[day][:capacity_left][need] }
cost = find_best_index(id, vehicle_routes[day])
end
cost = find_best_index(id, vehicle_routes[available_days[index]], false)
index += 1
end

[day, cost]
end

def find_best_cost(id, vehicle_id)
available_days = days_respecting_lapse(id, vehicle_id)
find_best_day_cost(available_days, @candidate_routes[vehicle_id], id)
cost
end

def compute_first_costs
costs = {}

@missing_visits.collect{ |vehicle, list|
list.collect{ |service_id|
day, cost = find_best_cost(service_id, vehicle)

next if cost.nil?

costs[service_id] = {
day: day,
vehicle: vehicle,
cost: cost
}
cost = find_best_day_cost(@candidate_routes[vehicle], service_id)
costs[service_id] = cost if cost
}
}

Expand Down Expand Up @@ -309,7 +284,7 @@ def reaffect_in_non_empty_route(still_removed)
referent_route ||= route_data
insertion_costs = compute_costs_for_route(route_data, remaining_ids)
insertion_costs.each{ |cost|
cost[:vehicle] = vehicle
cost[:vehicle] = vehicle_id
cost[:day] = day
}
insertion_costs
Expand All @@ -333,7 +308,7 @@ def reaffect_in_non_empty_route(still_removed)
insert_point_in_route(@candidate_routes[point_to_add[:vehicle]][point_to_add[:day]], point_to_add, false)
@output_tool&.add_single_visit(point_to_add[:day], @services_data[point_to_add[:id]][:used_days], point_to_add[:id], @services_data[point_to_add[:id]][:raw].visits_number)
still_removed.delete(still_removed.find{ |removed| removed.first == point_to_add[:id] })
@uninserted.delete(@uninserted.find{ |_id, data| data[:original_id] == to_plan[:service] }[0])
@uninserted.delete(@uninserted.find{ |_id, data| data[:original_id] == point_to_add[:id] }[0])
end
end

Expand Down
109 changes: 59 additions & 50 deletions lib/heuristics/scheduling_heuristic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -513,45 +513,23 @@ def compute_route_from(new_route, solver_route)
end

def compute_costs_for_route(route_data, set = nil)
vehicle_id = route_data[:vehicle_original_id]
day = route_data[:day]

### compute the cost, for each remaining service to assign, of assigning it to [route_data] ###
insertion_costs = []
set ||= @same_point_day ? @to_plan_service_ids.reject{ |id| @services_data[id][:raw].visits_number == 1 } : @to_plan_service_ids
# we will assign services with one vehicle in relaxed_same_point_day part
set.select{ |service_id|
# quantities are respected
((@same_point_day && @services_data[service_id][:group_capacity].all?{ |need, quantity| quantity <= route_data[:capacity_left][need] }) ||
(!@same_point_day && @services_data[service_id][:capacity].all?{ |need, quantity| quantity <= route_data[:capacity_left][need] })) &&
(@services_data[service_id][:sticky_vehicles_ids].empty? || @services_data[service_id][:sticky_vehicles_ids].include?(vehicle_id))
}.each{ |service_id|
next if @services_data[service_id][:used_days] && !days_respecting_lapse(service_id, vehicle_id).include?(day)

point = @services_data[service_id][:points_ids].first if @same_point_day || @relaxed_same_point_day # there can be only on point in points_ids
next if @relaxed_same_point_day &&
!@points_vehicles_and_days[point][:vehicles].empty? &&
(!@points_vehicles_and_days[point][:vehicles].include?(vehicle_id) || !(@points_vehicles_and_days[point][:maximum_visits_number] < @services_data[service_id][:raw].visits_number || @points_vehicles_and_days[point][:days].include?(day)))

next if @same_point_day && @unlocked.include?(service_id) && (!@points_vehicles_and_days[point][:vehicles].include?(vehicle_id) || !@points_vehicles_and_days[point][:days].include?(day))

period = @services_data[service_id][:heuristic_period]

next if !(period.nil? ||
route_data[:available_ids].include?(service_id) && (day + period..@schedule_end).step(period).find{ |current_day| @candidate_routes[vehicle_id][current_day.floor] && @candidate_routes[vehicle_id][current_day.floor][:completed] || @candidate_routes[vehicle_id][current_day.ceil] && @candidate_routes[vehicle_id][current_day.ceil][:completed] }.nil? &&
same_point_compatibility(service_id, vehicle_id, day))

next if two_visits_and_can_not_assign_second(vehicle_id, day, service_id)
### for each remaining service to assign, computes the cost of assigning it to [route_data] ###
unless set
set = @to_plan_service_ids
set.delete_if{ |id| @services_data[id][:raw].visits_number == 1 } if @same_point_day
end

other_indices = find_best_index(service_id, route_data)
insertion_costs << other_indices if other_indices
}
day = route_data[:day]
set.collect{ |service_id|
next if @services_data[service_id][:used_days] &&
!days_respecting_lapse(service_id, @candidate_routes[route_data[:vehicle_original_id]]).include?(day)

insertion_costs.compact
find_best_index(service_id, route_data)
}.compact
end

def two_visits_and_can_not_assign_second(vehicle_id, day, service_id)
return false unless @services_data[service_id][:raw].visits_number == 2 # || @end_phase ?
return false unless @services_data[service_id][:raw].visits_number == 2

next_day = day + @services_data[service_id][:heuristic_period]
day_to_insert = @candidate_routes[vehicle_id].keys.select{ |potential_day| potential_day >= next_day.round }.min
Expand Down Expand Up @@ -871,16 +849,52 @@ def compatible_days(service_id, day)
def compatible_vehicle(service_id, route_data)
# WARNING : this does not consider vehicle alternative skills properly
# we would need to know which skill_set is required in order that all services on same vehicle are compatible
route_data[:skills].any?{ |skill_set| (@services_data[service_id][:raw].skills - skill_set).empty? }
service_data = @services_data[service_id]
route_data[:skills].any?{ |skill_set| (service_data[:raw].skills - skill_set).empty? } &&
(service_data[:sticky_vehicles_ids].empty? || service_data[:sticky_vehicles_ids].include?(route_data[:vehicle_original_id]))
end

def service_compatible_with_route(service_id, route_data)
compatible_days(service_id, route_data[:day]) &&
compatible_vehicle(service_id, route_data)
def service_does_not_violate_capacity(service_id, route_data, first_visit)
needed_capacity = @services_data[service_id][:group_capacity] if first_visit && @same_point_day
needed_capacity ||= @services_data[service_id][:capacity] # if no same point day or not its group representative
needed_capacity.all?{ |need, quantity| quantity <= route_data[:capacity_left][need] }
end

def relaxed_or_same_point_day_constraint_respected(service_id, vehicle_id, day)
return true unless @same_point_day || @relaxed_same_point_day

# there can be only on point in points_ids because of these options :
point = @services_data[service_id][:points_ids].first

return true if @points_vehicles_and_days[point][:vehicles].empty?

if @relaxed_same_point_day
@points_vehicles_and_days[point][:vehicles].include?(vehicle_id) &&
(@points_vehicles_and_days[point][:days].include?(day) ||
@points_vehicles_and_days[point][:maximum_visits_number] < @services_data[service_id][:raw].visits_number)
else # @same_point_day is on :
!@unlocked.include?(service_id) ||
@points_vehicles_and_days[point][:vehicles].include?(vehicle_id) &&
@points_vehicles_and_days[point][:days].include?(day)
end
end

def service_compatible_with_route(service_id, route_data, first_visit)
vehicle_id = route_data[:vehicle_original_id]
day = route_data[:day]

compatible_days(service_id, day) &&
compatible_vehicle(service_id, route_data) &&
service_does_not_violate_capacity(service_id, route_data, first_visit) &&
(!first_visit ||
!two_visits_and_can_not_assign_second(vehicle_id, day, service_id) &&
route_data[:available_ids].include?(service_id) &&
relaxed_or_same_point_day_constraint_respected(service_id, vehicle_id, day) &&
same_point_compatibility(service_id, vehicle_id, day))
end

def find_best_index(service_id, route_data, first_visit = true)
return nil unless service_compatible_with_route(service_id, route_data)
return nil unless service_compatible_with_route(service_id, route_data, first_visit)

### find the best position in [route_data] to insert [service] ###
route = route_data[:stops]
Expand Down Expand Up @@ -1202,19 +1216,14 @@ def select_point(insertion_costs, empty_route)

def try_to_insert_at(vehicle_id, day, service_id, visit_number)
# when adjusting routes, tries to insert [service_id] at [day] for [vehicle]
if !@candidate_routes[vehicle_id][day][:completed] &&
@services_data[service_id][:capacity].all?{ |need, qty| @candidate_routes[vehicle_id][day][:capacity_left][need] - qty >= 0 } &&
@services_data[service_id][:sticky_vehicles_ids].empty? || @services_data[service_id][:sticky_vehicles_ids].include?(vehicle_id)
return if @candidate_routes[vehicle_id][day][:completed]

best_index = find_best_index(service_id, @candidate_routes[vehicle_id][day], false)
best_index = find_best_index(service_id, @candidate_routes[vehicle_id][day], false)
return unless best_index

if best_index
insert_point_in_route(@candidate_routes[vehicle_id][day], best_index, false)
@candidate_routes[vehicle_id][day][:stops].find{ |stop| stop[:id] == service_id }[:number_in_sequence] = visit_number

day
end
end
insert_point_in_route(@candidate_routes[vehicle_id][day], best_index, false)
@candidate_routes[vehicle_id][day][:stops].find{ |stop| stop[:id] == service_id }[:number_in_sequence] = visit_number
day
end

def find_corresponding_timewindow(day, arrival_time, timewindows, duration)
Expand Down
2 changes: 1 addition & 1 deletion test/real_cases_scheduling_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def test_performance_13vl

# voluntarily equal to watch evolution of scheduling algorithm performance
assert_equal expected, seen, 'Do not have the expected number of total visits'
assert_equal 294, unassigned_visits.sum, 'Do not have the expected number of unassigned visits'
assert_equal 292, unassigned_visits.sum, 'Do not have the expected number of unassigned visits'
end

def test_fill_days_and_post_processing
Expand Down

0 comments on commit 9275236

Please sign in to comment.