Skip to content

Commit

Permalink
Relation[:lapse] become relation[:lapses]
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsecadeline committed Aug 12, 2021
1 parent b87ec0f commit df05b13
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 104 deletions.
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.all?{ |l| l >= 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
33 changes: 32 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,30 @@ 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::NO_LAPSE_TYPES.include?(self.type)
[[self.linked_ids, self.linked_vehicle_ids, nil]]
elsif Models::Relation::ONE_LAPSE_TYPES.include?(self.type)
[[self.linked_ids, self.linked_vehicle_ids, self.lapses.first]]
elsif self.type == :vehicle_trips && self.lapses.nil?
[[nil, self.linked_vehicle_ids, nil]]
elsif self.lapses.uniq.size == 1
[[self.linked_ids, self.linked_vehicle_ids, self.lapses.first]]
else
[linked_ids, self.linked_vehicle_ids].collect.each_with_index{ |set, set_index|
set.collect.with_index{ |id, index|
next unless set[index + 1]

portion = Array.new(3, [])
portion[set_index] = [id, set[index + 1]]
portion[2] = self.lapses[index]
portion
}
}.flatten(1).compact
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

0 comments on commit df05b13

Please sign in to comment.