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

Fix internal vrp import #349

Merged
merged 9 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Allow to compute geojsons for synchronous resolutions [#356](https://github.com/Mapotempo/optimizer-api/pull/356/files)
- Calculate a vehicle_compatibility hash for each service and use it for unfeasible service detection [#318](https://github.com/Mapotempo/optimizer-api/pull/318)
- Add an enpoint able to validate the vrp send and return it "filtered" [#349](https://github.com/Mapotempo/optimizer-api/pull/349)

### Changed

Expand Down
9 changes: 5 additions & 4 deletions api/v01/entities/vrp_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ module VrpMisc
type: Array[Integer], values: ->(v) { v >= 0 },
desc: 'For some relation types, specifies duration or number constraint. Lapse expressed in days for minimum/maximum day lapse, in seconds for minimum/maximum_duration_lapse and vehicle_trips. For consistent relation types, lapse can be specified for every consecutive elements.')
mutually_exclusive :lapse, :lapses
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_ids, type: Array[String], documentation: { hidden: true }, allow_blank: false, desc: '[ DEPRECATED : use linked_service_ids ]', coerce_with: ->(val) { val.is_a?(String) ? val.split(/,/) : val })
optional(:linked_service_ids, type: Array[String], documentation: { hidden: true }, 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')
at_least_one_of :linked_ids, :linked_vehicle_ids
Expand Down Expand Up @@ -521,7 +522,7 @@ module VrpVehicles
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 :matrix_id, type: String, desc: 'Related matrix, if already defined'
optional :matrix_id, allow_blank: false, type: String, desc: 'Related matrix, if already defined'
optional :value_matrix_id, type: String, desc: 'If any value matrix defined, related matrix index'

optional(:duration, type: Integer, values: ->(v) { v.positive? }, desc: 'Maximum tour duration', coerce_with: ->(value) { ScheduleType.type_cast(value) })
Expand Down Expand Up @@ -590,8 +591,8 @@ module VrpVehicles
end

params :router_options do
optional :router_mode, type: String, desc: '`car`, `truck`, `bicycle`, etc... See the Router Wrapper API doc.'
exactly_one_of :matrix_id, :router_mode
optional :router_mode, allow_blank: false, type: String, desc: '`car`, `truck`, `bicycle`, etc... See the Router Wrapper API doc.'
at_least_one_of :matrix_id, :router_mode
braktar marked this conversation as resolved.
Show resolved Hide resolved
optional :router_dimension, type: String, values: ['time', 'distance'], desc: 'time or dimension, choose between a matrix based on minimal route duration or on minimal route distance'
optional :speed_multiplier, type: Float, default: 1.0, desc: 'Multiplies the vehicle speed, default : 1.0. Specifies if this vehicle is faster or slower than average speed.'
optional :area, type: Array, coerce_with: ->(c) { c.is_a?(String) ? c.split(/;|\|/).collect{ |b| b.split(',').collect{ |f| Float(f) } } : c }, desc: 'List of latitudes and longitudes separated with commas. Areas separated with pipes (available only for truck mode at this time).'
Expand Down
26 changes: 26 additions & 0 deletions api/v01/vrp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,32 @@ class Vrp < APIBase
end
end

resource :validate do
desc 'Validate VRP problem', {
nickname: 'validate_vrp',
success: [{
code: 200,
message: 'Vrp has been validated'
}],
failure: [{
code: 400,
message: 'Bad Request',
model: ::Api::V01::Status
}]
}
params {
use(:input)
}
post do
d_params = declared(params, include_missing: false) # Filtered in sentry if user_context
vrp_params = d_params[:points] ? d_params : d_params[:vrp]
::Models::Vrp.create(vrp_params)
present(d_params)
end
ensure
::Models.delete_all
end

resource :jobs do
desc 'Fetch vrp job status', {
nickname: 'get_job',
Expand Down
2 changes: 1 addition & 1 deletion lib/interpreters/compute_several_solutions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def self.find_best_heuristic(service_vrp)
custom_heuristics << 'supplied_initial_routes' if vrp.routes.any?

elapsed_times = []
vrp_hash = JSON.parse(service_vrp[:vrp].to_json, symbolize_names: true)
vrp_hash = service_vrp[:vrp].as_json
first_results = custom_heuristics.collect{ |heuristic|
s_vrp = duplicate_service_vrp(service_vrp, vrp_hash)
if heuristic == 'supplied_initial_routes'
Expand Down
26 changes: 13 additions & 13 deletions lib/interpreters/periodic_visits.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,18 @@ def generate_timewindows(timewindows_set, fixed_day_index = nil)

def generate_relations(vrp)
vrp.relations.flat_map{ |relation|
unless relation.linked_ids.uniq{ |s_id| @expanded_services[s_id]&.size.to_i }.size <= 1
raise "Cannot expand relations of #{relation.linked_ids} because they have different visits_number"
unless relation.linked_service_ids.uniq{ |s_id| @expanded_services[s_id]&.size.to_i }.size <= 1
raise "Cannot expand relations of #{relation.linked_service_ids} because they have different visits_number"
end

# keep the original relation if it is another type of relation or if it doesn't belong to an unexpanded service.
next relation if relation.linked_services.empty? || @expanded_services[relation.linked_ids.first]&.size.to_i == 0
next relation if relation.linked_services.empty? || @expanded_services[relation.linked_service_ids.first]&.size.to_i == 0

Array.new(@expanded_services[relation.linked_ids.first]&.size.to_i){ |visit_index|
linked_ids = relation.linked_ids.collect{ |s_id| @expanded_services[s_id][visit_index].id }
Array.new(@expanded_services[relation.linked_service_ids.first]&.size.to_i){ |visit_index|
linked_service_ids = relation.linked_service_ids.collect{ |s_id| @expanded_services[s_id][visit_index].id }

Models::Relation.create(
type: relation.type, linked_ids: linked_ids, lapses: relation.lapses, periodicity: relation.periodicity
type: relation.type, linked_service_ids: linked_service_ids, lapses: relation.lapses, periodicity: relation.periodicity
)
}
}
Expand All @@ -128,30 +128,30 @@ def generate_relations_between_visits(vrp, mission)
current_lapse = (index - 1) * mission.minimum_lapse.to_i
vrp.relations << Models::Relation.create(
type: :minimum_day_lapse,
linked_ids: ["#{mission.id}_1_#{mission.visits_number}", "#{mission.id}_#{index}_#{mission.visits_number}"],
linked_service_ids: ["#{mission.id}_1_#{mission.visits_number}", "#{mission.id}_#{index}_#{mission.visits_number}"],
lapses: [current_lapse]
)
}
2.upto(mission.visits_number){ |index|
current_lapse = (index - 1) * mission.maximum_lapse.to_i
vrp.relations << Models::Relation.create(
type: :maximum_day_lapse,
linked_ids: ["#{mission.id}_1_#{mission.visits_number}", "#{mission.id}_#{index}_#{mission.visits_number}"],
linked_service_ids: ["#{mission.id}_1_#{mission.visits_number}", "#{mission.id}_#{index}_#{mission.visits_number}"],
lapses: [current_lapse]
)
}
else
if mission.minimum_lapse
vrp.relations << Models::Relation.create(
type: :minimum_day_lapse,
linked_ids: 1.upto(mission.visits_number).map{ |index| "#{mission.id}_#{index}_#{mission.visits_number}" },
linked_service_ids: 1.upto(mission.visits_number).map{ |index| "#{mission.id}_#{index}_#{mission.visits_number}" },
lapses: [mission.minimum_lapse.to_i]
)
end
if mission.maximum_lapse
vrp.relations << Models::Relation.create(
type: :maximum_day_lapse,
linked_ids: 1.upto(mission.visits_number).map{ |index| "#{mission.id}_#{index}_#{mission.visits_number}" },
linked_service_ids: 1.upto(mission.visits_number).map{ |index| "#{mission.id}_#{index}_#{mission.visits_number}" },
lapses: [mission.maximum_lapse.to_i]
)
end
Expand Down Expand Up @@ -182,7 +182,7 @@ def generate_services(vrp)
first_possible_days: [service.first_possible_days[visit_index]],
last_possible_days: [service.last_possible_days[visit_index]]
)
new_service.skills += ["#{visit_index + 1}_f_#{service.visits_number}"] if !service.minimum_lapse && !service.maximum_lapse && service.visits_number > 1
new_service.skills += ["#{visit_index + 1}_f_#{service.visits_number}".to_sym] if !service.minimum_lapse && !service.maximum_lapse && service.visits_number > 1

@expanded_services[service.id] ||= []
@expanded_services[service.id] << new_service
Expand Down Expand Up @@ -397,10 +397,10 @@ def generate_rests(vehicle, day_index, vehicle_timewindow, rests_durations)

def associate_skills(new_vehicle, vehicle_day_index)
if new_vehicle.skills.empty?
new_vehicle.skills = [@periods.collect{ |period| "#{(vehicle_day_index * period / (@schedule_end + 1)).to_i + 1}_f_#{period}" }]
new_vehicle.skills = [@periods.collect{ |period| "#{(vehicle_day_index * period / (@schedule_end + 1)).to_i + 1}_f_#{period}".to_sym }]
else
new_vehicle.skills.collect!{ |alternative_skill|
alternative_skill + @periods.collect{ |period| "#{(vehicle_day_index * period / (@schedule_end + 1)).to_i + 1}_f_#{period}" }
alternative_skill + @periods.collect{ |period| "#{(vehicle_day_index * period / (@schedule_end + 1)).to_i + 1}_f_#{period}".to_sym }
}
end
end
Expand Down
53 changes: 26 additions & 27 deletions lib/interpreters/split_clustering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ def self.split_clusters(service_vrp, job = nil, &block)
route.stops.each do |stop|
next unless stop.service_id

stop.skills = stop.skills.to_a + ["cluster #{cluster_ref}"]
stop.skills = stop.skills.to_a + ["cluster #{cluster_ref}".to_sym]
end
}
solution.unassigned_stops.each do |stop|
next if stop.service_id.nil?

stop.skills = stop.skills.to_a + ["cluster #{cluster_ref}"]
stop.skills = stop.skills.to_a + ["cluster #{cluster_ref}".to_sym]
end
solution
}
Expand Down Expand Up @@ -149,7 +149,7 @@ def self.split_solve_candidate?(service_vrp)
ss_data[:representative_vrp].relations.each{ |rel|
raise 'There should be only :same_route relations inside the representative_vrp.' if rel.type != :same_route

current_vehicle_ids << rel.linked_ids.join if current_vehicle_ids.reject!{ |id| rel.linked_ids.include?(id) }
current_vehicle_ids << rel.linked_service_ids.join if current_vehicle_ids.reject!{ |id| rel.linked_service_ids.include?(id) }
}

current_vehicle_ids.size > 1 && (ss_data[:current_vehicle_limit] || Helper.fixnum_max) > 1 &&
Expand Down Expand Up @@ -360,7 +360,7 @@ def self.create_representative_vrp(split_solve_data)

vehicle_service_coordinates = vehicle_services.collect{ |s| [s.activity.point.location.lat, s.activity.point.location.lon] }
extreme_points = Helper.approximate_quadrilateral_polygon(vehicle_service_coordinates).uniq
linked_ids = ["0_representative_vrp_s_#{vehicle_id}"]
linked_service_ids = ["0_representative_vrp_s_#{vehicle_id}"]
extreme_points.each_with_index{ |lat_lon, index|
points << { id: "#{index + 1}_representative_vrp_p_#{vehicle_id}", location: { lat: lat_lon[0], lon: lat_lon[1] }}
visit_counts << 1
Expand All @@ -372,31 +372,31 @@ def self.create_representative_vrp(split_solve_data)
duration: 1
}
}
linked_ids << "#{index + 1}_representative_vrp_s_#{vehicle_id}"
linked_service_ids << "#{index + 1}_representative_vrp_s_#{vehicle_id}"
}

relations << { type: :same_route, linked_ids: linked_ids }
relations << { type: :same_route, linked_service_ids: linked_service_ids }
}

# go through the original relations and force the services and vehicles to stay in the same sub-vrp if necessary
split_solve_data[:original_vrp].relations.select{ |r| FORCING_RELATIONS.include?(r.type) }.each{ |relation|
if relation.linked_vehicle_ids.any? && relation.linked_services.none?
linked_ids = []
linked_service_ids = []
relation.linked_vehicle_ids.map{ |v_id|
s_id = "0_representative_vrp_s_#{v_id}"
linked_ids << s_id if services.any?{ |s| s[:id] == s_id }
linked_service_ids << s_id if services.any?{ |s| s[:id] == s_id }
}

relations << { type: :same_route, linked_ids: linked_ids } if linked_ids.size > 1
relations << { type: :same_route, linked_service_ids: linked_service_ids } if linked_service_ids.size > 1
elsif relation.linked_vehicle_ids.none? && relation.linked_services.any?
linked_ids = []
linked_service_ids = []
relation.linked_services.each{ |linked_service|
split_solve_data[:service_vehicle_assignments].any?{ |v_id, v_services|
linked_ids << "0_representative_vrp_s_#{v_id}" if v_services.include?(linked_service)
linked_service_ids << "0_representative_vrp_s_#{v_id}" if v_services.include?(linked_service)
}
}
linked_ids.uniq!
relations << { type: :same_route, linked_ids: linked_ids } if linked_ids.size > 1
linked_service_ids.uniq!
relations << { type: :same_route, linked_service_ids: linked_service_ids } if linked_service_ids.size > 1
else
# This shouldn't be possible
raise 'Unknown relation case in create_representative_vrp. If there is a new relation, update this function'
Expand Down Expand Up @@ -437,7 +437,7 @@ def self.create_representative_sub_vrp(split_solve_data)
}
r_sub_vrp.points = r_sub_vrp.services.map{ |s| s.activity.point }
r_sub_vrp.relations = representative_vrp.relations.select{ |relation|
r_sub_vrp.services.any?{ |s| s.id == relation.linked_ids[0] }
r_sub_vrp.services.any?{ |s| s.id == relation.linked_service_ids[0] }
}
r_sub_vrp
end
Expand Down Expand Up @@ -471,14 +471,14 @@ def self.transfer_unused_vehicles(split_solve_data)

def self.select_existing_relations(relations, vrp)
relations.select{ |relation|
next if relation.linked_vehicle_ids.empty? && relation.linked_ids.empty?
next if relation.linked_vehicle_ids.empty? && relation.linked_service_ids.empty?

(
relation.linked_vehicle_ids.empty? ||
relation.linked_vehicle_ids.any?{ |id| vrp.vehicles.any?{ |v| v.id == id } }
) && (
relation.linked_ids.empty? ||
relation.linked_ids.any?{ |id| vrp.services.any?{ |s| s.id == id } }
relation.linked_service_ids.empty? ||
relation.linked_service_ids.any?{ |id| vrp.services.any?{ |s| s.id == id } }
)
}
end
Expand Down Expand Up @@ -642,23 +642,22 @@ def self.build_partial_service_vrp(service_vrp, partial_service_ids, available_v
sub_vrp.units = vrp.units

sub_vrp.services = vrp.services.select{ |service| partial_service_ids.include?(service.id) }
rest_ids = sub_vrp.vehicles.flat_map{ |v| v.rests.map(&:id) }.uniq
sub_vrp.rests = vrp.rests.select{ |r| rest_ids.include?(r.id) }
sub_vrp.rests = sub_vrp.vehicles.flat_map{ |v| v.rests }.uniq
available_vehicle_ids = sub_vrp.vehicles.map(&:id)

sub_vrp.relations = vrp.relations.select{ |r|
next if r.type == :same_vehicle && entity == :vehicle

# Split should respect relations, it is enough to check only the first linked id -- [0..0].all? is to handle empties
r.linked_ids[0..0].all?{ |sid| partial_service_ids.include?(sid) } &&
r.linked_service_ids[0..0].all?{ |sid| partial_service_ids.include?(sid) } &&
r.linked_vehicle_ids[0..0].all?{ |vid| available_vehicle_ids.include?(vid) }
}

split_respects_relations = sub_vrp.relations.all?{ |r|
non_matching_linked_ids = r.linked_ids - partial_service_ids
non_matching_linked_service_ids = r.linked_service_ids - partial_service_ids
non_matching_linked_vehicle_ids = r.linked_vehicle_ids - available_vehicle_ids

(non_matching_linked_ids.empty? || non_matching_linked_ids.size == r.linked_ids.size) &&
(non_matching_linked_service_ids.empty? || non_matching_linked_service_ids.size == r.linked_service_ids.size) &&
(non_matching_linked_vehicle_ids.empty? || non_matching_linked_vehicle_ids.size == r.linked_vehicle_ids.size)
}
unless split_respects_relations
Expand All @@ -672,10 +671,10 @@ def self.build_partial_service_vrp(service_vrp, partial_service_ids, available_v
sub_vrp.points.uniq!
sub_vrp = add_corresponding_entity_skills(entity, sub_vrp)

sub_vrp_hash = JSON.parse(sub_vrp.to_json, symbolize_names: true)
sub_vrp_hash = sub_vrp.as_json

vehicle_ids = sub_vrp_hash[:vehicles]&.map{ |v| v[:id] } || []
sub_vrp_hash[:services].each{ |service|
sub_vrp_hash[:services]&.each{ |service|
braktar marked this conversation as resolved.
Show resolved Hide resolved
service[:sticky_vehicle_ids]&.delete_if{ |stick| !vehicle_ids.include?(stick) }
}

Expand Down Expand Up @@ -988,7 +987,7 @@ def self.collect_cluster_data(vrp, nb_clusters)
depot: depots[simulated_vehicle],
capacities: vehicles[simulated_vehicle][:capacities].collect{ |key, value| [key, value * vehicles.size / nb_clusters.to_f] }.to_h, #TODO: capacities needs a better way like depots...
skills: [],
day_skills: ['0_day_skill', '1_day_skill', '2_day_skill', '3_day_skill', '4_day_skill', '5_day_skill', '6_day_skill'],
day_skills: [:'0_day_skill', :'1_day_skill', :'2_day_skill', :'3_day_skill', :'4_day_skill', :'5_day_skill', :'6_day_skill'],
duration: 0,
total_work_days: 1
}
Expand Down Expand Up @@ -1049,11 +1048,11 @@ module ClassMethods
def compute_day_skills(timewindows)
if timewindows.nil? || timewindows.empty? || timewindows.any?{ |tw| tw[:day_index].nil? }
[0, 1, 2, 3, 4, 5, 6].collect{ |avail_day|
"#{avail_day}_day_skill"
"#{avail_day}_day_skill".to_sym
}
else
timewindows.collect{ |tw| tw[:day_index] }.uniq.collect{ |avail_day|
"#{avail_day}_day_skill"
"#{avail_day}_day_skill".to_sym
}
end
end
Expand Down
Loading