Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi tour #123

Merged
merged 6 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

### Added

- Implementation of `vehicle_trips` relation: the routes can be successive or with a minimum duration `lapse` in between [#123] (https://github.com/Mapotempo/optimizer-api/pull/123)

### Changed

### Removed

- Field `trips` in vehicle model. Use `vehicle_trips` relation instead [#123](https://github.com/Mapotempo/optimizer-api/pull/123)

### Fixed

## [v1.7.1] - 2021-05-20
Expand Down
6 changes: 3 additions & 3 deletions api/v01/entities/vrp_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,15 @@ module VrpMisc
shipment meetup
minimum_duration_lapse maximum_duration_lapse
force_first never_first force_end
vehicle_trips
vehicle_group_duration vehicle_group_duration_on_weeks
vehicle_group_duration_on_months vehicle_group_number],
desc: 'Relations allow to define constraints explicitly between activities and/or vehicles.
It could be the following types: same_route, sequence, order, minimum_day_lapse, maximum_day_lapse,
shipment, meetup, minimum_duration_lapse, maximum_duration_lapse')
shipment, meetup, minimum_duration_lapse, maximum_duration_lapse, vehicle_trips')
optional(:lapse,
type: Integer, values: ->(v) { v >= 0 },
desc: 'Only used for relations implying a duration constraint : minimum/maximum day lapse, vehicle group durations...')
desc: 'Only used for relations implying a duration constraint. Lapse expressed in days for minimum/maximum day lapse, in seconds for minimum/maximum_duration_lapse and vehicle_trips.')
optional(:linked_ids, type: Array[String], allow_blank: false, desc: 'List of activities involved in the relation', coerce_with: ->(val) { val.is_a?(String) ? val.split(/,/) : val })
optional(:linked_vehicle_ids, type: Array[String], allow_blank: false, desc: 'List of vehicles involved in the relation', coerce_with: ->(val) { val.is_a?(String) ? val.split(/,/) : val })
optional(:periodicity, type: Integer, documentation: { hidden: true }, desc: 'In the case of planning optimization, number of weeks/months to consider at the same time/in each relation : vehicle group duration on weeks/months')
Expand Down Expand Up @@ -489,7 +490,6 @@ module VrpVehicles
optional(:force_start, type: Boolean, documentation: { hidden: true }, desc: '[ DEPRECATED ]')
optional(:shift_preference, type: String, values: ['force_start', 'force_end', 'minimize_span'], desc: 'Force the vehicle to start as soon as the vehicle timewindow is open,
as late as possible or let vehicle start at any time. Not available with periodic heuristic, it will always leave as soon as possible.')
optional(:trips, type: Integer, default: 1, desc: 'The number of times a vehicle is allowed to return to the depot within its route. Not available with periodic heuristic.')
fonsecadeline marked this conversation as resolved.
Show resolved Hide resolved

optional :matrix_id, type: String, desc: 'Related matrix, if already defined'
optional :value_matrix_id, type: String, desc: 'If any value matrix defined, related matrix index'
Expand Down
57 changes: 0 additions & 57 deletions lib/interpreters/multi_trips.rb

This file was deleted.

108 changes: 49 additions & 59 deletions lib/interpreters/periodic_visits.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,7 @@ def initialize(vrp)
def expand(vrp, job, &block)
return vrp unless vrp.scheduling?

vehicles_linked_by_duration = save_relations(vrp, :vehicle_group_duration).concat(
save_relations(vrp, :vehicle_group_duration_on_weeks)
).concat(
save_relations(vrp, :vehicle_group_duration_on_months)
)
vehicles_linking_relations = save_vehicle_linking_relations(vrp)
vrp.relations = generate_relations(vrp)
vrp.rests = []
vrp.vehicles = generate_vehicles(vrp).sort{ |a, b|
Expand All @@ -61,8 +57,7 @@ def expand(vrp, job, &block)
vrp.shipments = generate_shipments(vrp)

@periods.uniq!
vehicles_linked_by_duration = get_all_vehicles_in_relation(vehicles_linked_by_duration)
generate_relations_on_periodic_vehicles(vrp, vehicles_linked_by_duration)
generate_relations_on_periodic_vehicles(vrp, vehicles_linking_relations)

if vrp.preprocessing_first_solution_strategy.to_a.first != 'periodic' && vrp.services.any?{ |service| service.visits_number > 1 }
vrp.routes = generate_routes(vrp)
Expand Down Expand Up @@ -247,7 +242,6 @@ def generate_vehicles(vrp)
timewindows = [vehicle.timewindow || vehicle.sequence_timewindows].flatten
if timewindows.empty?
new_vehicle = build_vehicle(vrp, vehicle, vehicle_day_index, rests_durations)
@equivalent_vehicles[vehicle.original_id] << new_vehicle.id
new_vehicle
else
timewindows.select{ |timewindow| timewindow.day_index.nil? || timewindow.day_index == vehicle_day_index % 7 }.collect{ |associated_timewindow|
Expand Down Expand Up @@ -456,67 +450,63 @@ def compute_possible_days(vrp)
}
end

def save_relations(vrp, relation_type)
vrp.relations.select{ |r| r.type == relation_type }.collect{ |r|
{
type: r.type,
linked_vehicle_ids: r.linked_vehicle_ids,
lapse: r.lapse,
periodicity: r.periodicity
}
def save_vehicle_linking_relations(vrp)
vrp.relations.select{ |r|
[:vehicle_group_duration, :vehicle_group_duration_on_weeks, :vehicle_group_duration_on_months,
:vehicle_trips].include?(r.type)
}
end

def get_all_vehicles_in_relation(relations)
relations&.each{ |r|
next if r[:type] == :vehicle_group_duration
def cut_linking_vehicle_relation_by_period(relation, periods, relation_type)
additional_relations = []
vehicles_in_relation =
relation[:linked_vehicle_ids].flat_map{ |v| @equivalent_vehicles[v] }

new_list = []
r[:linked_vehicle_ids].each{ |v|
new_list.concat(@equivalent_vehicles[v])
}
r[:linked_vehicle_ids] = new_list
}
while periods.any?
days_in_period = periods.slice!(0, relation.periodicity).flatten
relation_vehicles = vehicles_in_relation.select{ |id| days_in_period.include?(id.split('_').last.to_i) }
next unless relation_vehicles.any?

additional_relations << Models::Relation.new(
linked_vehicle_ids: relation_vehicles,
lapse: relation.lapse,
type: relation_type
)
end

additional_relations
end

def generate_relations_on_periodic_vehicles(vrp, list)
new_relations = []
list.each{ |r|
case r[:type]
def collect_weeks_in_schedule
current_day = (@schedule_start + 1..@schedule_start + 7).find{ |d| d % 7 == 0 } # next monday
schedule_week_indices = [(@schedule_start..current_day - 1).to_a]
while current_day + 6 <= @schedule_end
schedule_week_indices << (current_day..current_day + 6).to_a
current_day += 7
end
schedule_week_indices << (current_day..@schedule_end).to_a unless current_day > @schedule_end

schedule_week_indices
end

def generate_relations_on_periodic_vehicles(vrp, vehicle_linking_relations)
vrp.relations.concat(vehicle_linking_relations.flat_map{ |relation|
case relation[:type]
when :vehicle_group_duration
new_relations << [r[:linked_vehicle_ids], r[:lapse]]
Models::Relation.new(
type: :vehicle_group_duration, lapse: relation.lapse,
linked_vehicle_ids: relation[:linked_vehicle_ids].flat_map{ |v| @equivalent_vehicles[v] })
when :vehicle_group_duration_on_weeks
current_sub_list = []
first_index = r[:linked_vehicle_ids].min.split('_').last.to_i
in_periodicity = first_index + 7 * (r[:periodicity] - 1)
max_index = (in_periodicity..in_periodicity + 7).find{ |index| index % 7 == 6 }
r[:linked_vehicle_ids].sort_by{ |v_id| v_id.split('_').last.to_i }.each{ |v_id|
this_index = v_id.split('_').last.to_i
if this_index <= max_index
current_sub_list << v_id
else
new_relations << [current_sub_list, r[:lapse]]
current_sub_list = [v_id]
first_index = this_index
in_periodicity = first_index + 7 * (r[:periodicity] - 1)
max_index = (in_periodicity..in_periodicity + 7).find{ |index| index % 7 == 6 }
end
}
new_relations << [current_sub_list, r[:lapse]]
schedule_week_indices = collect_weeks_in_schedule
cut_linking_vehicle_relation_by_period(relation, schedule_week_indices, :vehicle_group_duration)
when :vehicle_group_duration_on_months
(0..vrp.schedule_months_indices.size - 1).step(r[:periodicity]).collect{ |v| p vrp.schedule_months_indices.slice(v, v + r[:periodicity]).flatten }.each{ |month_indices|
new_relations << [r[:linked_vehicle_ids].select{ |id| month_indices.include?(id.split('_').last.to_i) }, r[:lapse]]
}
cut_linking_vehicle_relation_by_period(relation, vrp.schedule_months_indices, :vehicle_group_duration)
when :vehicle_trips
# we want want vehicle_trip relation per day :
all_days = (@schedule_start..@schedule_end).to_a
cut_linking_vehicle_relation_by_period(relation, all_days, :vehicle_trips)
end
}

new_relations.each{ |linked_vehicle_ids, lapse|
vrp.relations << Models::Relation.new(
type: :vehicle_group_duration,
linked_vehicle_ids: linked_vehicle_ids,
lapse: lapse
)
}
})
end

private
Expand Down
110 changes: 109 additions & 1 deletion models/concerns/validate_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,109 @@ def check_shipments_specificities
}
end

def calculate_day_availabilities(vehicles, timewindow_arrays)
vehicles_days = timewindow_arrays.collect{ |timewindows|
if timewindows.empty?
[]
else
days = timewindows.flat_map{ |tw| tw[:day_index] || (0..6).to_a }
days.compact!
days.uniq
end
}.delete_if(&:empty?)

vehicles_unavailable_indices = vehicles.collect{ |v| v[:unavailable_work_day_indices] }
vehicles_unavailable_indices.compact!

[vehicles_days, vehicles_unavailable_indices]
end

def check_vehicle_trips_stores_consistency(relation_vehicles)
relation_vehicles.each_with_index{ |vehicle_trip, v_i|
case v_i
when 0
unless vehicle_trip[:end_point_id]
raise OptimizerWrapper::DiscordantProblemError.new('First trip should at least have an end point id')
end
when relation_vehicles.size - 1
unless vehicle_trip[:start_point_id]
raise OptimizerWrapper::DiscordantProblemError.new('Last trip should at least have a start point id')
end
else
unless vehicle_trip[:start_point_id] && vehicle_trip[:end_point_id]
raise OptimizerWrapper::DiscordantProblemError.new(
'Intermediary trips should have a start and an end point ids'
)
end
end

next if v_i.zero? ||
vehicle_trip[:start_point_id] == relation_vehicles[v_i - 1][:end_point_id] ||
(@hash[:points].find{ |pt| pt[:id] == vehicle_trip[:start_point_id] }[:location] ==
@hash[:points].find{ |pt| pt[:id] == relation_vehicles[v_i - 1][:end_point_id] }[:location])

raise OptimizerWrapper::DiscordantProblemError.new('One trip should start where the previous trip ended')
}
end

def check_trip_timewindows_consistency(relation_vehicles)
vehicles_timewindows =
relation_vehicles.collect{ |v| v[:timewindow] ? [v[:timewindow]] : v[:sequence_timewindows].to_a }

return if vehicles_timewindows.all?(&:empty?)

week_days, unavailable_indices = calculate_day_availabilities(relation_vehicles, vehicles_timewindows)
if week_days.uniq.size > 1 || unavailable_indices.uniq.size > 1
raise OptimizerWrapper::UnsupportedProblemError.new(
'Vehicles in vehicle_trips relation should have the same available days'
)
end

vehicles_timewindows.each_with_index{ |v_tw, v_i|
next if v_i.zero?

v_tw.each{ |tw|
day = tw[:day_index]
fonsecadeline marked this conversation as resolved.
Show resolved Hide resolved
previous_tw =
vehicles_timewindows[v_i - 1].select{ |ptw|
ptw[:day_index].nil? ||
ptw[:day_index] == day
}.min_by{ |ptw| ptw[:start] }

unless previous_tw.nil? || tw[:end] > previous_tw[:start]
raise OptimizerWrapper::DiscordantProblemError.new('Timewindows do not allow vehicle trips')
end
}
}
end

def check_relations(periodic_heuristic)
return unless @hash[:relations].to_a.any?

@hash[:relations].group_by{ |relation| relation[:type] }.each{ |type, relations|
case type
when 'vehicle_trips'
relations.each{ |relation|
relation_vehicles =
relation[:linked_vehicle_ids].to_a.collect{ |v_id| @hash[:vehicles].find{ |v| v[:id] == v_id } }

if relation_vehicles.empty?
raise OptimizerWrapper::DiscordantProblemError.new(
'A non empty list of vehicles IDs should be provided for vehicle_trips relations'
)
elsif relation_vehicles.any?(&:nil?)
# FIXME: linked_vehicle_ids should be directly related to vehicle objects of the model
raise OptimizerWrapper::DiscordantProblemError.new(
fonsecadeline marked this conversation as resolved.
Show resolved Hide resolved
'At least one vehicle ID in relations does not match with any provided vehicle'
)
end

check_vehicle_trips_stores_consistency(relation_vehicles)
check_trip_timewindows_consistency(relation_vehicles)
}
end
}

# shipment relation consistency
if @hash[:relations]&.any?{ |r| r[:type] == :shipment }
shipment_relations = @hash[:relations].select{ |r| r[:type] == :shipment }
Expand Down Expand Up @@ -200,9 +300,17 @@ def check_configuration(configuration, periodic_heuristic)
end

def check_clustering_parameters(configuration)
if @hash[:relations].to_a.any?{ |relation| relation[:type] == 'vehicle_trips' }
if configuration[:preprocessing][:partitions]&.any?
raise OptimizerWrapper::UnsupportedProblemError.new(
'Partitioning is not currently available with vehicle_trips relation'
)
end
end

return unless configuration[:preprocessing][:partitions]&.any?{ |partition|
partition[:entity].to_sym == :work_day
}
} && configuration[:schedule]

if @hash[:services].any?{ |s|
min_lapse = s[:minimum_lapse]&.floor || 1
Expand Down
2 changes: 0 additions & 2 deletions models/vehicle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ class Vehicle < Base

field :force_start, default: false
field :shift_preference, default: :minimize_span
field :trips, default: 1
field :duration, default: nil
field :overall_duration, default: nil
field :distance, default: nil
Expand Down Expand Up @@ -92,7 +91,6 @@ class Vehicle < Base
# validates_numericality_of :global_day_index, allow_nil: true
# validates_inclusion_of :router_dimension, in: %w( time distance )
# validates_inclusion_of :shift_preference, in: %w( force_start force_end minimize_span )
# validates_numericality_of :trips, greater_than_or_equal_to: 0
# validates_numericality_of :speed_multiplier
# validates_numericality_of :duration, greater_than_or_equal_to: 0
# validates_numericality_of :overall_duration, greater_than_or_equal_to: 0
Expand Down
Loading