Skip to content

Commit 311330d

Browse files
committed
Pyvrp introduce shift preferences using depot timewindows
1 parent ba4e390 commit 311330d

1 file changed

Lines changed: 107 additions & 33 deletions

File tree

wrappers/pyvrp.rb

Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def solver_constraints
2424
:assert_no_ride_constraint,
2525
:assert_no_service_duration_modifiers,
2626
:assert_vehicles_no_alternative_skills,
27-
:assert_vehicles_no_force_start,
27+
:assert_vehicles_no_force_start, # Use shift_preference instead
2828
:assert_vehicles_no_initial_load,
2929
:assert_vehicles_no_late_multiplier,
3030
:assert_vehicles_no_overload_multiplier,
@@ -173,7 +173,7 @@ def read_depot_end(vrp, vehicle)
173173
end
174174

175175
def read_reload_depot_trip(vrp, vehicle, reload_depot_index)
176-
reload_depot = @reload_depots[reload_depot_index]
176+
reload_depot = @reload_depots[@depots.size - reload_depot_index]
177177
return nil if reload_depot.nil?
178178

179179
route_data = compute_route_data(vrp, vehicle, reload_depot.point)
@@ -224,6 +224,11 @@ def pyvrp_problem(vrp)
224224

225225
# Skills can be considered as capacities
226226
@skills_index_hash = {}
227+
228+
# to keep the client and depot indices consistent, the depots should be built before the clients and the matrices
229+
@point_hash = vrp.points.index_by(&:id)
230+
depots = build_depots(vrp)
231+
227232
vrp.vehicles.map(&:skills).flatten.uniq.each_with_index{ |skill, index| @skills_index_hash[skill] = index }
228233
used_matrices = vrp.vehicles.map(&:matrix_id).uniq
229234
matrices = used_matrices.map { |id| vrp.matrices.find { |m| m.id == id } }
@@ -233,9 +238,6 @@ def pyvrp_problem(vrp)
233238

234239
distance_matrices = duration_matrices if distance_matrices.empty?
235240

236-
# to keep the client indices consistent, the depots should be built before the clients
237-
depots = build_depots(vrp)
238-
239241
@reload_depot_index_hash = {}
240242
vrp.reload_depots.each_with_index{ |depot, index| @reload_depot_index_hash[depot.id] = depots.size + index }
241243
clients, groups = build_clients_and_groups(vrp)
@@ -253,11 +255,7 @@ def pyvrp_problem(vrp)
253255
end
254256

255257
def expand_matrices(vrp, distance_matrices, duration_matrices)
256-
depot_points =
257-
vrp.vehicles.flat_map{ |veh|
258-
[veh.start_point, veh.end_point]
259-
}.uniq
260-
additive_setups = Array.new(depot_points.size, 0)
258+
additive_setups = Array.new(@depots.size, 0)
261259

262260
reload_depot_points =
263261
vrp.reload_depots.map(&:point)
@@ -272,7 +270,7 @@ def expand_matrices(vrp, distance_matrices, duration_matrices)
272270
points
273271
}
274272

275-
all_points = (depot_points + reload_depot_points + client_points)
273+
all_points = (@depots + reload_depot_points + client_points)
276274

277275
distance_matrices.map! do |matrix|
278276
matrix =
@@ -304,10 +302,6 @@ def distance(matrix, point1, point2)
304302

305303
def build_vehicles(vrp)
306304
used_matrices = vrp.vehicles.map(&:matrix_id).uniq
307-
@depot_index_hash =
308-
vrp.vehicles.flat_map{ |veh|
309-
[veh.start_point, veh.end_point]
310-
}.uniq.each_with_index.map { |pt, idx| [pt&.id, idx] }.to_h
311305
all_units = vrp.units.index_by(&:id)
312306

313307
vrp.vehicles.map { |veh|
@@ -325,8 +319,8 @@ def build_vehicles(vrp)
325319
{
326320
num_available: 1,
327321
capacity: capacity_hash.values + capacity_skills,
328-
start_depot: @depot_index_hash[veh.start_point&.id],
329-
end_depot: @depot_index_hash[veh.end_point&.id],
322+
start_depot: @vehicle_start_point_index_hash[veh.id],
323+
end_depot: @vehicle_end_point_index_hash[veh.id],
330324
fixed_cost: veh.cost_fixed.to_i,
331325
tw_early: veh.timewindow&.start || 0,
332326
tw_late: veh.timewindow&.end || MAX_INT64,
@@ -336,7 +330,7 @@ def build_vehicles(vrp)
336330
unit_duration_cost: veh.cost_time_multiplier.to_i,
337331
profile: used_matrices.index(veh.matrix_id),
338332
start_late: nil,
339-
reload_depots: veh.reload_depots.map{ |depot| @depot_hash[depot.id] },
333+
reload_depots: veh.reload_depots.map{ |depot| @reload_depot_hash[depot.id] },
340334
max_reloads: veh.maximum_reloads || 0,
341335
name: veh.id.to_s
342336
}
@@ -414,26 +408,106 @@ def build_clients_and_groups(vrp)
414408
[client_list, groups]
415409
end
416410

411+
def add_depot_point(point, index_hash, criteria = nil)
412+
return if point.nil?
413+
414+
return index_hash[point.id] if index_hash.key?(point.id) && index_hash[point.id].is_a?(Integer)
415+
416+
return index_hash[point.id][criteria] if index_hash[point.id].is_a?(Hash) && index_hash[point.id].key?(criteria)
417+
418+
@depots << point
419+
if criteria
420+
index_hash[point.id] ||= {}
421+
index_hash[point.id][criteria] = index_hash[point.id].size
422+
else
423+
index_hash[point.id] = index_hash.size
424+
end
425+
end
426+
417427
def build_depots(vrp)
418-
depot_points = vrp.vehicles.flat_map { |vehicle| [vehicle.start_point, vehicle.end_point] }.uniq
419-
@reload_depots = Array.new(depot_points.size, nil)
420-
@depot_hash = {}
421-
@depot_vehicle_hash = {}
422-
depots =
423-
depot_points.map do |point|
428+
@depots = []
429+
@vehicle_start_point_index_hash = {}
430+
@vehicle_end_point_index_hash = {}
431+
@depot_points_standard_index_hash = {}
432+
@depot_points_force_start_by_timewindow_start_index_hash = {}
433+
@depot_points_force_end_by_timewindow_end_index_hash = {}
434+
vrp.vehicles.group_by(&:shift_preference).each do |shift_preference, vehicles|
435+
vehicles.group_by(&:timewindow).each do |timewindow, sub_vehicles|
436+
case shift_preference
437+
when :force_start
438+
sub_vehicles.each do |vehicle|
439+
@vehicle_start_point_index_hash[vehicle.id] =
440+
add_depot_point(
441+
vehicle.start_point,
442+
@depot_points_force_start_by_timewindow_start_index_hash,
443+
timewindow.start
444+
)
445+
@vehicle_end_point_index_hash[vehicle.id] =
446+
add_depot_point(vehicle.end_point, @depot_points_standard_index_hash)
447+
end
448+
when :force_end
449+
sub_vehicles.each do |vehicle|
450+
@vehicle_start_point_index_hash[vehicle.id] =
451+
add_depot_point(vehicle.start_point, @depot_points_standard_index_hash)
452+
@vehicle_end_point_index_hash[vehicle.id] =
453+
add_depot_point(
454+
vehicle.end_point,
455+
@depot_points_force_end_by_timewindow_end_index_hash,
456+
timewindow.end
457+
)
458+
end
459+
when :minimize_span
460+
sub_vehicles.each do |vehicle|
461+
@vehicle_start_point_index_hash[vehicle.id] =
462+
add_depot_point(vehicle.start_point, @depot_points_standard_index_hash)
463+
@vehicle_end_point_index_hash[vehicle.id] =
464+
add_depot_point(vehicle.end_point, @depot_points_standard_index_hash)
465+
end
466+
end
467+
end
468+
end
469+
depots = Array.new(@depots.size, nil)
470+
@depot_points_standard_index_hash.map { |point_id, index|
471+
depots[index] =
424472
{
425-
x: point&.location&.lon || 0,
426-
y: point&.location&.lat || 0,
473+
x: @point_hash[point_id]&.location&.lon || 0,
474+
y: @point_hash[point_id]&.location&.lat || 0,
427475
tw_early: 0,
428476
tw_late: MAX_INT64,
429-
name: point&.id&.to_s || '_null_store'
477+
name: "#{point_id}_standard" || '_null_store'
430478
}
431-
end
479+
}
480+
@depot_points_force_start_by_timewindow_start_index_hash.each{ |point_id, (timewindow_start, point_indices)|
481+
point_indices.map { |point_index|
482+
depots[point_index] =
483+
{
484+
x: @point_hash[point_id]&.location&.lon || 0,
485+
y: @point_hash[point_id]&.location&.lat || 0,
486+
tw_early: timewindow_start || 0,
487+
tw_late: timewindow_start,
488+
name: "#{point_id}_#{timewindow_start}_force_start" || '_null_store'
489+
}
490+
}
491+
}
492+
@depot_points_force_end_by_timewindow_end_index_hash.keys.flat_map { |point_id, (timewindow_end, point_indices)|
493+
point_indices.map { |point_index|
494+
depots[point_index] = {
495+
x: @point_hash[point_id]&.location&.lon || 0,
496+
y: @point_hash[point_id]&.location&.lat || 0,
497+
tw_early: timewindow_end || 0,
498+
tw_late: timewindow_end || MAX_INT64,
499+
name: "#{point_id}_#{timewindow_end}_force_end" || '_null_store'
500+
}
501+
}
502+
}
503+
504+
@reload_depots = []
505+
@reload_depot_hash = {}
432506
vrp.reload_depots.each do |depot|
433-
next if @depot_hash.key?(depot.id)
507+
next if @reload_depot_hash.key?(depot.id)
434508

435509
@reload_depots << depot
436-
@depot_hash[depot.id] = depot_points.size
510+
@reload_depot_hash[depot.id] = @depots.size
437511
depots <<
438512
{
439513
x: depot.point&.location&.lon || 0,
@@ -465,18 +539,18 @@ def build_routes(vrp)
465539
def build_trips(vrp, route, vehicle_type)
466540
trips = []
467541
vehicle = vrp.vehicles[vehicle_type]
468-
end_depot = @depot_index_hash[vehicle.end_point&.id]
542+
end_depot = @vehicle_end_point_index_hash[vehicle.id]
469543
current_trip = {
470544
visits: [],
471545
vehicle_type: vehicle_type,
472-
start_depot: @depot_index_hash[vehicle.start_point&.id],
546+
start_depot: @vehicle_start_point_index_hash[vehicle.id],
473547
end_depot: end_depot
474548
}
475549
route.missions.each do |mission|
476550
if mission.is_a?(Models::Service)
477551
current_trip[:visits] << @service_index_map.find_index{ |service| service && service.id == mission.id }
478552
elsif mission.is_a?(Models::ReloadDepot)
479-
reload_depot = @depot_hash[mission.id]
553+
reload_depot = @reload_depot_hash[mission.id]
480554
current_trip[:end_depot] = reload_depot
481555
trips << current_trip
482556
current_trip = {

0 commit comments

Comments
 (0)