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

Relation[:lapse] become relation[:lapses] #265

Merged
merged 2 commits into from
Aug 13, 2021
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 @@ -12,6 +12,7 @@
### Changed

- Improve cases where a service has two visits in periodic heuristic: ensure that the second visit can be assigned to the right day [#227](https://github.com/Mapotempo/optimizer-api/pull/227)
- Relation `lapse` becomes `lapses` : we can now provide a specific lapse for every consecutive ID in the relation [#265](https://github.com/Mapotempo/optimizer-api/pull/265)

### Removed

Expand Down
6 changes: 5 additions & 1 deletion api/v01/entities/vrp_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,11 @@ module VrpMisc
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. Lapse expressed in days for minimum/maximum day lapse, in seconds for minimum/maximum_duration_lapse and vehicle_trips.')
desc: '[ DEPRECATED ]')
optional(:lapses,
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_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
18 changes: 9 additions & 9 deletions lib/interpreters/periodic_visits.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def generate_relations(vrp)
linked_ids = relation.linked_ids.collect{ |s_id| @expanded_services[s_id][visit_index].id }

Models::Relation.create(
type: relation.type, linked_ids: linked_ids, lapse: relation.lapse, periodicity: relation.periodicity
type: relation.type, linked_ids: linked_ids, lapses: relation.lapses, periodicity: relation.periodicity
)
}
}
Expand All @@ -117,15 +117,15 @@ def generate_relations_between_visits(vrp, mission)
vrp.relations << Models::Relation.create(
type: :minimum_day_lapse,
linked_ids: ["#{mission.id}_1_#{mission.visits_number}", "#{mission.id}_#{index}_#{mission.visits_number}"],
lapse: current_lapse
lapses: [current_lapse]
)
}
(2..mission.visits_number).each{ |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}"],
lapse: current_lapse
lapses: [current_lapse]
)
}
elsif mission.minimum_lapse
Expand All @@ -134,7 +134,7 @@ def generate_relations_between_visits(vrp, mission)
vrp.relations << Models::Relation.create(
type: :minimum_day_lapse,
linked_ids: ["#{mission.id}_#{index - 1}_#{mission.visits_number}", "#{mission.id}_#{index}_#{mission.visits_number}"],
lapse: current_lapse
lapses: [current_lapse]
)
}
elsif mission.maximum_lapse
Expand All @@ -143,7 +143,7 @@ def generate_relations_between_visits(vrp, mission)
vrp.relations << Models::Relation.create(
type: :maximum_day_lapse,
linked_ids: ["#{mission.id}_#{index - 1}_#{mission.visits_number}", "#{mission.id}_#{index}_#{mission.visits_number}"],
lapse: current_lapse
lapses: [current_lapse]
)
}
end
Expand Down Expand Up @@ -230,7 +230,7 @@ def generate_vehicles(vrp)
new_relation = Models::Relation.create(
type: :vehicle_group_duration,
linked_vehicle_ids: @equivalent_vehicles[vehicle.original_id],
lapse: vehicle.overall_duration + rests_durations[index]
lapses: [vehicle.overall_duration + rests_durations[index]]
)
vrp.relations << new_relation
end
Expand Down Expand Up @@ -316,7 +316,7 @@ def generate_routes(vrp)
last_computed_time: 0
}
}
residual_time.push(r[:lapse])
residual_time.push(r.lapses.first)
idx += 1
}

Expand Down Expand Up @@ -439,7 +439,7 @@ def cut_linking_vehicle_relation_by_period(relation, periods, relation_type)

additional_relations << Models::Relation.create(
linked_vehicle_ids: relation_vehicles,
lapse: relation.lapse,
lapses: relation.lapses,
type: relation_type
)
end
Expand All @@ -466,7 +466,7 @@ def generate_relations_on_periodic_vehicles(vrp, vehicle_linking_relations)
Models::Relation.create(
type: :vehicle_group_duration,
linked_vehicle_ids: relation[:linked_vehicle_ids].flat_map{ |v| @equivalent_vehicles[v] },
lapse: relation.lapse
lapses: relation.lapses
)
when :vehicle_group_duration_on_weeks
schedule_week_indices = collect_weeks_in_schedule
Expand Down
87 changes: 67 additions & 20 deletions models/concerns/validate_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,30 +264,77 @@ def check_trip_timewindows_consistency(relation_vehicles)
}
end

def check_consistent_ids_provided(relation)
case relation[:type]
when *Models::Relation::ON_VEHICLES_TYPES
if relation[:linked_ids].to_a.any? || relation[:linked_vehicle_ids].to_a.empty?
raise OptimizerWrapper::DiscordantProblemError.new(
"#{relation[:type]} relations do not support linked_ids and expect linked_vehicle_ids"
)
end
when *Models::Relation::ON_SERVICES_TYPES
if relation[:linked_vehicle_ids].to_a.any? || relation[:linked_ids].to_a.empty?
raise OptimizerWrapper::DiscordantProblemError.new(
"#{relation[:type]} relations do not support linked_vehicle_ids and expect linked_ids"
)
end
else
raise 'Unknown relation type' # in case we ever forget to handle a new relation type
end
end

def check_consistent_lapses_provided(relation)
case relation[:type]
when *Models::Relation::NO_LAPSE_TYPES
if relation[:lapse] || relation[:lapses].to_a.any?
raise OptimizerWrapper::DiscordantProblemError.new(
"#{relation[:type]} relations do not expect any lapse"
)
end
when *Models::Relation::ONE_LAPSE_TYPES
if relation[:lapse].nil? && relation[:lapses].to_a.size != 1
raise OptimizerWrapper::DiscordantProblemError.new(
"#{relation[:type]} relations expect exactly one lapse"
)
end
when *Models::Relation::SEVERAL_LAPSE_TYPES
relation[:lapses] = [0] if relation[:type] == :vehicle_trips && relation[:lapse].nil? && relation[:lapses].nil?
if relation[:lapse].nil? && relation[:lapses].to_a.size != 1
exp_lapse_count = (relation[:linked_ids]&.size || relation[:linked_vehicle_ids]&.size) - 1
if relation[:lapses].to_a.size != exp_lapse_count
raise OptimizerWrapper::DiscordantProblemError.new(
"#{relation[:type]} relations expect at least one lapse"
)
end
end
else
raise 'Unknown relation type' # in case we ever forget to handle a new relation type
end
end

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

@hash[:relations].group_by{ |relation| relation[:type] }.each{ |type, relations|
case type.to_sym
@hash[:relations].each{ |relation|
relation_services =
relation[:linked_ids].to_a.collect{ |s_id| @hash[:services].find{ |s| s[:id] == s_id } }
relation_vehicles =
relation[:linked_vehicle_ids].to_a.collect{ |v_id| @hash[:vehicles].find{ |v| v[:id] == v_id } }

if relation_vehicles.any?(&:nil?) || relation_services.any?(&:nil?)
# FIXME: linked_vehicle_ids should be directly related to vehicle objects of the model
raise OptimizerWrapper::DiscordantProblemError.new(
'At least one ID in relations does not match with any provided vehicle or service from the data'
)
end

check_consistent_ids_provided(relation)
check_consistent_lapses_provided(relation)

case relation[:type].to_sym
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(
'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)
}
check_vehicle_trips_stores_consistency(relation_vehicles)
check_trip_timewindows_consistency(relation_vehicles)
end
}

Expand Down
26 changes: 25 additions & 1 deletion models/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@

module Models
class Relation < Base
NO_LAPSE_TYPES = %i[same_vehicle same_route sequence order shipment meetup force_first never_first force_end].freeze
ONE_LAPSE_TYPES = %i[vehicle_group_number vehicle_group_duration vehicle_group_duration_on_weeks vehicle_group_duration_on_months].freeze
SEVERAL_LAPSE_TYPES = %i[minimum_day_lapse maximum_day_lapse minimum_duration_lapse maximum_duration_lapse vehicle_trips].freeze
ON_VEHICLES_TYPES = %i[vehicle_group_number vehicle_group_duration vehicle_group_duration_on_weeks vehicle_group_duration_on_months vehicle_trips].freeze
ON_SERVICES_TYPES = %i[same_vehicle same_route sequence order shipment meetup force_first never_first force_end minimum_day_lapse maximum_day_lapse minimum_duration_lapse maximum_duration_lapse].freeze

field :type, default: :same_route
field :lapse, default: nil
field :lapses, default: nil
field :linked_ids, default: []
has_many :linked_services, class_name: 'Models::Service'
field :linked_vehicle_ids, default: []
Expand Down Expand Up @@ -49,5 +55,23 @@ def self.create(hash)
hash[:type] = hash[:type]&.to_sym if hash.key?(:type)
super(hash)
end

def split_regarding_lapses
# TODO : can we create relations from here ?
# remove self.linked_ids
if Models::Relation::SEVERAL_LAPSE_TYPES.include?(self.type)
if self.lapses.uniq.size == 1
[[self.linked_ids, self.linked_vehicle_ids, self.lapses.first]]
else
self.lapses.collect.with_index{ |lapse, index|
[self.linked_ids && self.linked_ids[index..index+1],
self.linked_vehicle_ids && self.linked_vehicle_ids[index..index+1],
lapse]
}
end
else
[[self.linked_ids, self.linked_vehicle_ids, self.lapses&.first]]
end
end
end
end
31 changes: 21 additions & 10 deletions models/vrp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def self.convert_shipments_to_services(hash)
max_lapse = shipment[:maximum_inroute_duration]
next unless max_lapse

hash[:relations] << { type: :maximum_duration_lapse, linked_ids: @linked_ids[shipment[:id]], lapse: max_lapse }
hash[:relations] << { type: :maximum_duration_lapse, linked_ids: @linked_ids[shipment[:id]], lapses: [max_lapse] }
}
convert_shipment_within_routes(hash)
hash.delete(:shipments)
Expand All @@ -186,7 +186,8 @@ def self.convert_relations_of_shipment_to_services(hash, shipment_id, pickup_ser
when :minimum_duration_lapse, :maximum_duration_lapse
relation[:linked_ids][0] = delivery_service_id if relation[:linked_ids][0] == shipment_id
relation[:linked_ids][1] = pickup_service_id if relation[:linked_ids][1] == shipment_id
relation[:lapse] ||= 0
relation[:lapses] ||= relation[:lapse] ? [relation[:lapse]] : [0]
relation.delete(:lapse)
when :same_route, :same_vehicle
relation[:linked_ids].each_with_index{ |id, id_i|
next unless id == shipment_id
Expand Down Expand Up @@ -328,13 +329,31 @@ def self.convert_geometry_polylines_to_geometry(hash)
hash[:configuration][:restitution].delete(:geometry_polyline)
end

def self.convert_relation_lapse_into_lapses(hash)
hash[:relations].to_a.each{ |relation|
if Models::Relation::ONE_LAPSE_TYPES.include?(relation[:type])
if relation[:lapse]
relation[:lapses] = [relation[:lapse]]
relation.delete(:lapse)
end
elsif [:vehicle_trips, Models::Relation::SEVERAL_LAPSE_TYPES].flatten.include?(relation[:type])
if relation[:lapse]
expected_size = relation[:linked_vehicle_ids].to_a.size + relation[:linked_ids].to_a.size - 1
relation[:lapses] = Array.new(expected_size, relation[:lapse]) if expected_size > 0
relation.delete(:lapse)
end
end
}
end

def self.ensure_retrocompatibility(hash)
self.convert_position_relations(hash)
self.deduce_first_solution_strategy(hash)
self.deduce_minimum_duration(hash)
self.deduce_solver_parameter(hash)
self.convert_route_indice_into_index(hash)
self.convert_geometry_polylines_to_geometry(hash)
self.convert_relation_lapse_into_lapses(hash)
end

def self.filter(hash)
Expand Down Expand Up @@ -376,14 +395,6 @@ def self.remove_unnecessary_units(hash)
def self.remove_unnecessary_relations(hash)
return unless hash[:relations]&.any?

types_with_duration =
%i[minimum_day_lapse maximum_day_lapse
minimum_duration_lapse maximum_duration_lapse
vehicle_group_duration vehicle_group_duration_on_weeks
vehicle_group_duration_on_months vehicle_group_number]

hash[:relations].delete_if{ |r| r[:lapse].nil? && types_with_duration.include?(r[:type]) }

# TODO : remove this filter, VRP with duplicated relations should not be accepted
uniq_relations = []
hash[:relations].group_by{ |r| r[:type] }.each{ |_type, relations_set|
Expand Down
8 changes: 4 additions & 4 deletions test/lib/interpreters/interpreter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1398,7 +1398,7 @@ def test_overall_duration_several_vehicles
problem[:relations] = [{
type: :vehicle_group_duration_on_weeks,
linked_vehicle_ids: ['vehicle_0', 'vehicle_1'],
lapse: 10,
lapses: [10],
periodicity: 1
}]
problem[:configuration][:schedule] = {
Expand Down Expand Up @@ -1428,7 +1428,7 @@ def test_overall_duration_with_periodicity
problem[:relations] = [{
type: :vehicle_group_duration_on_weeks,
linked_vehicle_ids: ['vehicle_0', 'vehicle_1'],
lapse: 10,
lapses: [10],
periodicity: 2
}]
problem[:configuration][:schedule] = {
Expand All @@ -1452,7 +1452,7 @@ def test_expand_relations_of_one_week_and_one_day
problem[:relations] = [{
type: :vehicle_group_duration_on_weeks,
linked_vehicle_ids: ['vehicle_0'],
lapse: 10
lapses: [10]
}]
problem[:configuration][:schedule] = {
range_indices: { start: 0, end: 7 }
Expand All @@ -1476,7 +1476,7 @@ def test_expand_relations_of_one_month_and_one_day
problem[:relations] = [{
type: :vehicle_group_duration_on_months,
linked_vehicle_ids: ['vehicle_0'],
lapse: 10
lapses: [10]
}]
problem[:configuration][:schedule] = {
range_date: { start: Date.new(2020, 1, 1), end: Date.new(2020, 2, 1) }
Expand Down
Loading