Skip to content

Commit ba4e390

Browse files
committed
PyVRP with initial routes
1 parent cc473eb commit ba4e390

12 files changed

Lines changed: 189 additions & 58 deletions

File tree

lib/heuristics/dichotomous_approach.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,16 @@ def self.build_initial_routes(solutions)
250250
next if solution.nil?
251251

252252
solution.routes.map{ |route|
253-
mission_ids = route.stops.map(&:service_id).compact
254-
next if mission_ids.empty?
253+
missions = route.stops.map{ |stop|
254+
next if stop.is_a?(Models::Solution::StopDepot) || stop.mission.is_a?(Models::Rest)
255+
256+
stop.mission
257+
}.compact
258+
next if missions.empty?
255259

256260
Models::Route.create(
257261
vehicle: route.vehicle,
258-
mission_ids: mission_ids
262+
missions: missions
259263
)
260264
}
261265
}.compact

lib/heuristics/periodic_heuristic.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,7 @@ def add_same_freq_located_points(best_index, route_data)
890890
@services_assignment[service_id][:vehicles] |= [route_data[:vehicle_original_id]]
891891
route_data[:stops].insert(best_index[:position] + i + 1,
892892
id: service_id,
893+
mission: @services_data[service_id][:raw],
893894
point_id: best_index[:point],
894895
start: start,
895896
arrival: start,
@@ -1398,7 +1399,7 @@ def prepare_output_and_collect_routes(vrp)
13981399

13991400
vrp_routes << {
14001401
vehicle_id: vrp_vehicle.id,
1401-
mission_ids: computed_stops.collect{ |stop| stop[:service_id] }.compact
1402+
missions: computed_stops.map{ |stop| stop[:mission] }.compact
14021403
}
14031404

14041405
solution_routes << Models::Solution::Route.new(stops: computed_stops,
@@ -1552,10 +1553,10 @@ def construct_sub_vrp(vrp, vehicle, current_route)
15521553
route_vrp
15531554
end
15541555

1555-
def generate_route(vehicle, services)
1556+
def generate_route(vehicle, stops)
15561557
{
15571558
vehicle: vehicle,
1558-
mission_ids: services.collect{ |service| service[:id] }
1559+
missions: stops.map{ |stop| stop[:mission] }.compact
15591560
}
15601561
end
15611562

lib/interpreters/multi_trip.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def presolve(service_vrp, job = nil, &block)
3737
solution.unassigned_stops = []
3838

3939
vehicles = under_used_routes.map(&:vehicle)
40-
reload_depots = vehicles.flat_map(&:reload_depots)
40+
reload_depots = vehicles.flat_map(&:reload_depots).uniq
4141
points =
4242
vehicles.map(&:start_point) +
4343
vehicles.map(&:end_point) +

models/activity.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
# along with Mapotempo. If not, see:
1616
# <http://www.gnu.org/licenses/agpl.html>
1717
#
18-
require './models/base'
18+
require './models/mission'
1919

2020
module Models
21-
class Activity < Base
21+
class Activity < Mission
2222
field :duration, default: 0
2323
field :setup_duration, default: 0
2424
field :additional_value, default: 0

models/mission.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright © Cartoway, 2025
2+
#
3+
# This file is part of Cartoway Optimizer.
4+
#
5+
# Cartoway Planner is free software. You can redistribute it and/or
6+
# modify since you respect the terms of the GNU Affero General
7+
# Public License as published by the Free Software Foundation,
8+
# either version 3 of the License, or (at your option) any later version.
9+
#
10+
# Cartoway Optimizer is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12+
# or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
13+
#
14+
# You should have received a copy of the GNU Affero General Public License
15+
# along with Cartoway Optimizer. If not, see:
16+
# <http://www.gnu.org/licenses/agpl.html>
17+
#
18+
19+
require './models/base'
20+
21+
module Models
22+
class Mission < Base; end
23+
end

models/route.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,25 @@ class Route < Base
2222
field :mission_ids, default: []
2323
field :day_index
2424
belongs_to :vehicle, class_name: 'Models::Vehicle', as_json: :id
25+
26+
has_many :missions, class_name: 'Models::Mission', as_json: :ids
27+
28+
def initialize(hash)
29+
hash[:missions] ||= []
30+
if hash[:mission_ids].present?
31+
hash[:missions] +=
32+
hash[:mission_ids]&.map{ |mission_id|
33+
Models::Service.find_by_id(mission_id) ||
34+
Models::ReloadDepot.find_by_id(mission_id) ||
35+
Models::Rest.find_by_id(mission_id)
36+
}&.compact
37+
hash.delete(:mission_ids)
38+
end
39+
super(hash)
40+
end
41+
42+
def mission_ids
43+
missions.map{ |mission| mission.original_id || mission.id }
44+
end
2545
end
2646
end

models/service.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
require './models/base'
1919

2020
module Models
21-
class Service < Base
21+
class Service < Mission
2222
field :id
2323
field :original_id, default: nil
2424

models/solution/parsers/stop_parser.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def self.parse(service, options)
4242
alternative: options[:index], # nil if unassigned but return by default the last activity
4343
loads: build_loads(service, options),
4444
activity: dup_activity,
45+
mission: service,
4546
info: options[:info] || Models::Solution::Stop::Info.new({}),
4647
reason: options[:reason],
4748
skills: options[:skills] || service.skills,
@@ -88,6 +89,7 @@ def self.parse(reload_depot, options)
8889
type: :reload_depot,
8990
loads: options[:loads],
9091
activity: Models::ReloadDepot.new(reload_depot.as_json),
92+
mission: reload_depot,
9193
info: options[:info] || Models::Solution::Stop::Info.new({})
9294
}
9395
end
@@ -100,6 +102,7 @@ def self.parse(rest, options)
100102
rest_id: rest.original_id || rest.id,
101103
type: :rest,
102104
activity: Models::Rest.new(rest.as_json),
105+
mission: rest,
103106
info: options[:info] || Models::Solution::Stop::Info.new({})
104107
}
105108
end

models/solution/stop.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Stop < Base
3737
field :exclusion_cost
3838

3939
has_many :loads, class_name: 'Models::Solution::Load'
40+
belongs_to :mission, class_name: 'Models::Mission', vrp_result: :hide
4041
belongs_to :activity, class_name: 'Models::Activity'
4142
belongs_to :info, class_name: 'Models::Solution::Stop::Info', vrp_result: :hide
4243

wrappers/ortools.rb

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def solve(vrp, job, thread_proc = nil, &block)
8585
}.each{ |relation|
8686
order_route = {
8787
vehicle: vrp.vehicles.size == 1 ? vrp.vehicles.first : nil,
88-
mission_ids: relation.linked_service_ids
88+
missions: relation.linked_services
8989
}
9090
vrp.routes += [order_route]
9191
}
@@ -136,7 +136,7 @@ def solve(vrp, job, thread_proc = nil, &block)
136136
@job = job
137137
@previous_result = nil
138138
relations = []
139-
services = []
139+
ortools_services = []
140140
routes = []
141141
services_activity_positions = { always_first: [], always_last: [], never_first: [], never_last: [] }
142142
vrp.services.each_with_index{ |service, service_index|
@@ -161,7 +161,7 @@ def solve(vrp, job, thread_proc = nil, &block)
161161
}
162162

163163
if service.activity
164-
services << OrtoolsVrp::Service.new(
164+
ortools_services << OrtoolsVrp::Service.new(
165165
time_windows: service.activity.timewindows.collect{ |tw|
166166
OrtoolsVrp::TimeWindow.new(start: tw.start, end: tw.end || 2147483647,
167167
maximum_lateness: tw.maximum_lateness)
@@ -208,11 +208,12 @@ def solve(vrp, job, thread_proc = nil, &block)
208208
alternative_index: 0
209209
)
210210

211-
services = update_services_activity_positions(services, services_activity_positions, service.id,
212-
service.activity.position, service_index, 0)
211+
ortools_services =
212+
update_services_activity_positions(ortools_services, services_activity_positions, service.id,
213+
service.activity.position, service_index, 0)
213214
elsif service.activities
214215
service.activities.each_with_index{ |possible_activity, activity_index|
215-
services << OrtoolsVrp::Service.new(
216+
ortools_services << OrtoolsVrp::Service.new(
216217
time_windows: possible_activity.timewindows.collect{ |tw|
217218
OrtoolsVrp::TimeWindow.new(start: tw.start, end: tw.end || 2147483647,
218219
maximum_lateness: tw.maximum_lateness)
@@ -257,8 +258,9 @@ def solve(vrp, job, thread_proc = nil, &block)
257258
alternative_index: activity_index
258259
)
259260

260-
services = update_services_activity_positions(services, services_activity_positions, service.id,
261-
possible_activity.position, service_index, activity_index)
261+
ortools_services =
262+
update_services_activity_positions(ortools_services, services_activity_positions, service.id,
263+
possible_activity.position, service_index, activity_index)
262264
}
263265
end
264266
}
@@ -275,17 +277,17 @@ def solve(vrp, job, thread_proc = nil, &block)
275277
}
276278

277279
vehicles = build_problem_vehicles(vrp, total_quantities)
278-
build_problem_relations(vrp, services, relations)
280+
build_problem_relations(vrp, ortools_services, relations)
279281

280282
vrp.routes.collect{ |route|
281-
next if route.vehicle.nil? || route.mission_ids.empty?
283+
next if route.vehicle.nil? || route.missions.empty?
282284

283-
service_ids = corresponding_mission_ids(services.collect(&:id), route.mission_ids)
284-
next if service_ids.empty?
285+
ortools_service_ids = corresponding_mission_ids(ortools_services, route.missions)
286+
next if ortools_service_ids.empty?
285287

286288
routes << OrtoolsVrp::Route.new(
287289
vehicle_id: route.vehicle.id.to_s,
288-
service_ids: service_ids.map(&:to_s)
290+
service_ids: ortools_service_ids.map(&:to_s)
289291
)
290292
}
291293

@@ -308,7 +310,7 @@ def solve(vrp, job, thread_proc = nil, &block)
308310

309311
problem = OrtoolsVrp::Problem.new(
310312
vehicles: vehicles,
311-
services: services,
313+
services: ortools_services,
312314
matrices: matrices,
313315
relations: relations,
314316
routes: routes
@@ -710,8 +712,9 @@ def update_services_activity_positions(services, services_activity_positions,
710712
}
711713
end
712714

713-
def corresponding_mission_ids(available_ids, mission_ids)
714-
mission_ids.collect{ |mission_id|
715+
def corresponding_mission_ids(available_ortools_services, missions)
716+
available_ids = available_ortools_services.map(&:id)
717+
missions.map(&:id).collect{ |mission_id|
715718
correct_id =
716719
if available_ids.include?(mission_id)
717720
mission_id

0 commit comments

Comments
 (0)