Skip to content

Commit

Permalink
Provide rest timewindows compatible with vehicle
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsecadeline authored and Braktar committed Feb 9, 2022
1 parent 8146c61 commit 68c5a53
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 13 deletions.
38 changes: 25 additions & 13 deletions lib/interpreters/periodic_visits.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,14 @@ def expand(vrp, job, &block)
vrp
end

def generate_timewindows(timewindows_set)
def generate_timewindows(timewindows_set, fixed_day_index = nil)
return nil if timewindows_set.empty?

timewindows_set.flat_map{ |timewindow|
if @have_day_index
if @have_day_index && fixed_day_index
[Models::Timewindow.create(start: timewindow.start + fixed_day_index * 86400,
end: (timewindow.end || 86400) + fixed_day_index * 86400)]
elsif @have_day_index
first_day =
if timewindow.day_index
(@schedule_start..@schedule_end).find{ |day| day % 7 == timewindow.day_index }
Expand Down Expand Up @@ -185,13 +188,13 @@ def generate_services(vrp)
}.flatten
end

def build_vehicle(vrp, vehicle, vehicle_day_index, rests_durations)
def build_vehicle(vrp, vehicle, vehicle_day_index, vehicle_timewindow, rests_durations)
new_vehicle = duplicate_safe(
vehicle,
id: "#{vehicle.id}_#{vehicle_day_index}",
global_day_index: vehicle_day_index,
skills: associate_skills(vehicle, vehicle_day_index),
rests: generate_rests(vehicle, vehicle_day_index, rests_durations),
rests: generate_rests(vehicle, vehicle_day_index, vehicle_timewindow, rests_durations),
sequence_timewindows: [],
)
@equivalent_vehicles[vehicle.id] << new_vehicle.id
Expand All @@ -212,12 +215,18 @@ 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)
new_vehicle
build_vehicle(vrp, vehicle, vehicle_day_index, nil, rests_durations)
else
timewindows.select{ |timewindow| timewindow.day_index.nil? || timewindow.day_index == vehicle_day_index % 7 }.collect{ |associated_timewindow|
new_vehicle = build_vehicle(vrp, vehicle, vehicle_day_index, rests_durations)
new_vehicle.timewindow = Models::Timewindow.create(start: associated_timewindow.start || 0, end: associated_timewindow.end || 86400)
timewindows.select{ |timewindow|
timewindow.day_index.nil? || timewindow.day_index == vehicle_day_index % 7
}.collect{ |associated_timewindow|
# FIXME : this function assumes there are no two timewindows with same day_index.
# If two timewindows have same day index then we will generate two vehicles with same id.
new_vehicle = build_vehicle(vrp, vehicle, vehicle_day_index, associated_timewindow, rests_durations)
new_vehicle.timewindow = Models::Timewindow.create(
start: associated_timewindow.start || 0,
end: associated_timewindow.end || 86400
)
if @have_day_index
new_vehicle.timewindow.start += vehicle_day_index * 86400
new_vehicle.timewindow.end += vehicle_day_index * 86400
Expand Down Expand Up @@ -365,15 +374,18 @@ def generate_routes(vrp)
routes
end

def generate_rests(vehicle, day_index, rests_durations)
def generate_rests(vehicle, day_index, vehicle_timewindow, rests_durations)
vehicle.rests.collect{ |rest|
next unless rest.timewindows.empty? || rest.timewindows.any?{ |timewindow| timewindow.day_index.nil? || timewindow.day_index == day_index % 7 }
# Rests can not have more than one timewindow for now
next if (vehicle_timewindow && rest.timewindows.first &&
!rest.timewindows.first.compatible_with?(vehicle_timewindow)) ||
(rest.timewindows.first&.day_index && rest.timewindows.first.day_index != day_index % 7)

# rest is compatible with this vehicle day
# rest is compatible with this vehicle day and timewindow
new_rest = Marshal.load(Marshal.dump(rest))
new_rest.id = "#{new_rest.id}_#{day_index + 1}"
rests_durations[-1] += new_rest.duration
new_rest.timewindows = generate_timewindows(rest.timewindows)
new_rest.timewindows = generate_timewindows(rest.timewindows, day_index)
new_rest
}.compact
end
Expand Down
11 changes: 11 additions & 0 deletions models/timewindow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,16 @@ def self.create(hash)

super(hash)
end

def compatible_with?(timewindow, check_days = true)
raise unless timewindow.is_a?(Models::Timewindow)

return false if check_days &&
self.day_index && timewindow.day_index &&
self.day_index != timewindow.day_index

(self.end.nil? || timewindow.start.nil? || timewindow.start <= self.end + (self.maximum_lateness || 0)) &&
(self.start.nil? || timewindow.end.nil? || timewindow.end + (timewindow.maximum_lateness || 0) >= self.start)
end
end
end
23 changes: 23 additions & 0 deletions test/lib/interpreters/interpreter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1617,4 +1617,27 @@ def test_expand_multitrips_relations
assert_equal 4, expanded_vrp.vehicles.size
assert_equal 2, (expanded_vrp.relations.count{ |r| r[:type] == :vehicle_trips })
end

def test_expand_rests_timewindows
problem = VRP.lat_lon_two_vehicles
problem[:rests] = [{ id: 'rest', duration: 10 }]
problem[:vehicles].each{ |v| v[:rest_ids] = ['rest'] }
problem[:configuration][:schedule] = { range_indices: { start: 0, end: 3 }}

expanded_vrp = periodic_expand(problem)
assert_equal 8, expanded_vrp.vehicles.size
assert_equal (1..4).map{ |day| "rest_#{day}" }, expanded_vrp.vehicles.flat_map(&:rest_ids).uniq

problem[:rests].first[:timewindows] = [{ start: 2, end: 3 }]
expanded_vrp = periodic_expand(problem)
assert_equal (1..4).map{ |day| "rest_#{day}" }, expanded_vrp.vehicles.flat_map(&:rest_ids).uniq
assert(expanded_vrp.vehicles.all?{ |v| v.rests.first.timewindows.first.start == 2 })
assert(expanded_vrp.vehicles.all?{ |v| v.rests.first.timewindows.first.end == 3 })

problem[:rests].first[:timewindows].first[:day_index] = 1
expanded_vrp = periodic_expand(problem)
assert_equal ['rest_2'], expanded_vrp.vehicles.flat_map(&:rest_ids).uniq
vehicle_at_day_one = expanded_vrp.vehicles.find{ |v| v.global_day_index == 1 }
assert_equal 86402, vehicle_at_day_one.rests.first.timewindows.first.start
end
end
44 changes: 44 additions & 0 deletions test/models/timewindow_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright © Mapotempo, 2021
#
# This file is part of Mapotempo.
#
# Mapotempo is free software. You can redistribute it and/or
# modify since you respect the terms of the GNU Affero General
# Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# Mapotempo is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Mapotempo. If not, see:
# <http://www.gnu.org/licenses/agpl.html>
#
require './test/test_helper'

module Models
class TimewindowTest < Minitest::Test
def test_compatibility_between_timewindows
tw1 = Models::Timewindow.new(start: 10, end: 20, day_index: 0)
assert_raises RuntimeError do
tw1.compatible_with?([0, 10], false)
end
assert tw1.compatible_with?(tw1, false)

tw2 = Models::Timewindow.new(start: 10, end: 20, day_index: 1)
assert tw1.compatible_with?(tw2, false)
refute tw1.compatible_with?(tw2)
refute tw1.compatible_with?(tw2, true) # lateness has no impact on days incompatibility

# ignore days :
tw2.start = 21
tw2.end = 25
refute tw1.compatible_with?(tw2, false)
tw2.start = 5
assert tw1.compatible_with?(tw2, false)
tw2.end = 8
refute tw1.compatible_with?(tw2, false)
end
end
end

0 comments on commit 68c5a53

Please sign in to comment.