Skip to content

Commit 3653bee

Browse files
committed
Pyvrp introduce shift preferences using depot timewindows
1 parent ba4e390 commit 3653bee

1 file changed

Lines changed: 99 additions & 33 deletions

File tree

wrappers/pyvrp.rb

Lines changed: 99 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,98 @@ 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(vehicle.start_point, @depot_points_force_start_by_timewindow_start_index_hash, timewindow.start)
441+
@vehicle_end_point_index_hash[vehicle.id] =
442+
add_depot_point(vehicle.end_point, @depot_points_standard_index_hash)
443+
end
444+
when :force_end
445+
sub_vehicles.each do |vehicle|
446+
@vehicle_start_point_index_hash[vehicle.id] =
447+
add_depot_point(vehicle.start_point, @depot_points_standard_index_hash)
448+
@vehicle_end_point_index_hash[vehicle.id] =
449+
add_depot_point(vehicle.end_point, @depot_points_force_end_by_timewindow_end_index_hash, timewindow.end)
450+
end
451+
when :minimize_span
452+
sub_vehicles.each do |vehicle|
453+
@vehicle_start_point_index_hash[vehicle.id] =
454+
add_depot_point(vehicle.start_point, @depot_points_standard_index_hash)
455+
@vehicle_end_point_index_hash[vehicle.id] =
456+
add_depot_point(vehicle.end_point, @depot_points_standard_index_hash)
457+
end
458+
end
459+
end
460+
end
461+
depots = Array.new(@depots.size, nil)
462+
@depot_points_standard_index_hash.map { |point_id, index|
463+
depots[index] =
424464
{
425-
x: point&.location&.lon || 0,
426-
y: point&.location&.lat || 0,
465+
x: @point_hash[point_id]&.location&.lon || 0,
466+
y: @point_hash[point_id]&.location&.lat || 0,
427467
tw_early: 0,
428468
tw_late: MAX_INT64,
429-
name: point&.id&.to_s || '_null_store'
469+
name: "#{point_id}_standard" || '_null_store'
430470
}
431-
end
471+
}
472+
@depot_points_force_start_by_timewindow_start_index_hash.each{ |point_id, (timewindow_start, point_indices)|
473+
point_indices.map { |point_index|
474+
depots[point_index] =
475+
{
476+
x: @point_hash[point_id]&.location&.lon || 0,
477+
y: @point_hash[point_id]&.location&.lat || 0,
478+
tw_early: timewindow_start || 0,
479+
tw_late: timewindow_start,
480+
name: "#{point_id}_#{timewindow_start}_force_start" || '_null_store'
481+
}
482+
}
483+
}
484+
@depot_points_force_end_by_timewindow_end_index_hash.keys.flat_map { |point_id, (timewindow_end, point_indices)|
485+
point_indices.map { |point_index|
486+
depots[point_index] = {
487+
x: @point_hash[point_id]&.location&.lon || 0,
488+
y: @point_hash[point_id]&.location&.lat || 0,
489+
tw_early: timewindow_end || 0,
490+
tw_late: timewindow_end || MAX_INT64,
491+
name: "#{point_id}_#{timewindow_end}_force_end" || '_null_store'
492+
}
493+
}
494+
}
495+
496+
@reload_depots = []
497+
@reload_depot_hash = {}
432498
vrp.reload_depots.each do |depot|
433-
next if @depot_hash.key?(depot.id)
499+
next if @reload_depot_hash.key?(depot.id)
434500

435501
@reload_depots << depot
436-
@depot_hash[depot.id] = depot_points.size
502+
@reload_depot_hash[depot.id] = @depots.size
437503
depots <<
438504
{
439505
x: depot.point&.location&.lon || 0,
@@ -465,18 +531,18 @@ def build_routes(vrp)
465531
def build_trips(vrp, route, vehicle_type)
466532
trips = []
467533
vehicle = vrp.vehicles[vehicle_type]
468-
end_depot = @depot_index_hash[vehicle.end_point&.id]
534+
end_depot = @vehicle_end_point_index_hash[vehicle.id]
469535
current_trip = {
470536
visits: [],
471537
vehicle_type: vehicle_type,
472-
start_depot: @depot_index_hash[vehicle.start_point&.id],
538+
start_depot: @vehicle_start_point_index_hash[vehicle.id],
473539
end_depot: end_depot
474540
}
475541
route.missions.each do |mission|
476542
if mission.is_a?(Models::Service)
477543
current_trip[:visits] << @service_index_map.find_index{ |service| service && service.id == mission.id }
478544
elsif mission.is_a?(Models::ReloadDepot)
479-
reload_depot = @depot_hash[mission.id]
545+
reload_depot = @reload_depot_hash[mission.id]
480546
current_trip[:end_depot] = reload_depot
481547
trips << current_trip
482548
current_trip = {

0 commit comments

Comments
 (0)