Skip to content

Commit f56bbeb

Browse files
author
Braktar
committed
Fix activities and relations
1 parent bdd4014 commit f56bbeb

File tree

11 files changed

+186
-17
lines changed

11 files changed

+186
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Allow to compute geojsons for synchronous resolutions [#356](https://github.com/Mapotempo/optimizer-api/pull/356/files)
88
- Calculate a vehicle_compatibility hash for each service and use it for unfeasible service detection [#318](https://github.com/Mapotempo/optimizer-api/pull/318)
99
- Add an enpoint able to validate the vrp send and return it "filtered" [#349](https://github.com/Mapotempo/optimizer-api/pull/349)
10+
- Activity positions and linking relations tolerates alternatives [#392](https://github.com/Mapotempo/optimizer-api/pull/392)
1011

1112
### Changed
1213

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ bundle install
6060

6161
This project requires some solver and interface projects in order to be fully functional!
6262
* [Vroom v1.8.0](https://github.com/VROOM-Project/vroom/releases/tag/v1.8.0)
63-
* [Optimizer-ortools v1.12.0](https://github.com/Mapotempo/optimizer-ortools) & [OR-Tools v7.8](https://github.com/google/or-tools/releases/tag/v7.8) (use the version corresponding to your system operator, not source code).
63+
* [Optimizer-ortools v1.14.0](https://github.com/Mapotempo/optimizer-ortools) & [OR-Tools v7.8](https://github.com/google/or-tools/releases/tag/v7.8) (use the version corresponding to your system operator, not source code).
6464

6565
Note : when updating OR-Tools you should to recompile optimizer-ortools.
6666

docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ARG VROOM_VERSION
88
FROM vroomvrp/vroom-docker:${VROOM_VERSION:-v1.8.0} as vroom
99

1010
# Rake
11-
FROM ${REGISTRY:-registry.mapotempo.com/}mapotempo-${BRANCH:-ce}/optimizer-ortools:${OPTIMIZER_ORTOOLS_VERSION:-v1.13.0}
11+
FROM ${REGISTRY:-registry.mapotempo.com/}mapotempo-${BRANCH:-ce}/optimizer-ortools:${OPTIMIZER_ORTOOLS_VERSION:-v1.14.0}
1212
ARG BUNDLE_WITHOUT
1313

1414
ENV LANG C.UTF-8

models/concerns/validate_data.rb

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,17 +203,25 @@ def check_position_relation_specificities
203203
@hash[:services].find{ |service| service[:id] == linked_id }
204204
}
205205
previous_service = nil
206+
previous_activities = []
206207
services.each{ |service|
207-
if previous_service && forbidden_position_pairs.include?([previous_service[:activity][:position], service[:activity][:position]])
208-
inconsistent_position_services << [previous_service[:id], service[:id]]
209-
end
208+
activities = service[:activity] ? [service[:activity]] : service[:activities]
209+
210+
previous_activities.each{ |previous_activity|
211+
activities.each{ |activity|
212+
next unless forbidden_position_pairs.include?([previous_activity[:position], activity[:position]])
213+
214+
inconsistent_position_services << [previous_service[:id], service[:id]]
215+
}
216+
}
210217
previous_service = service
218+
previous_activities = activities
211219
}
212220
}
213221

214222
return unless inconsistent_position_services.any?
215223

216-
raise OptimizerWrapper::DiscordantProblemError.new("Inconsistent positions in relations: #{inconsistent_position_services}")
224+
raise OptimizerWrapper::DiscordantProblemError.new("Inconsistent positions in relations: #{inconsistent_position_services.uniq}")
217225
end
218226

219227
def calculate_day_availabilities(vehicles, timewindow_arrays)
@@ -405,6 +413,7 @@ def check_relations(periodic_heuristic)
405413
check_position_relation_specificities
406414
check_relation_consistent_ids
407415
check_sticky_relation_consistency
416+
check_relation_compatibility_with_alternatives
408417

409418
if periodic_heuristic
410419
incompatible_relation_types = @hash[:relations].collect{ |r| r[:type] }.uniq - %i[force_first never_first force_end same_vehicle]
@@ -484,6 +493,25 @@ def check_sticky_relation_consistency
484493
)
485494
end
486495

496+
def check_relation_compatibility_with_alternatives
497+
not_handled_relations = []
498+
@hash[:relations].each{ |relation|
499+
next if Models::Relation::ALTERNATIVE_COMPATIBLE_RELATIONS.include?(relation[:type])
500+
501+
services = @hash[:services].select{ |service| relation[:linked_service_ids]&.include?(service[:id]) }
502+
not_handled_relations << relation if services.any?{ |service|
503+
(service[:activities]&.size || 0) > 1
504+
}
505+
}
506+
507+
return unless not_handled_relations.any?
508+
509+
raise OptimizerWrapper::UnsupportedProblemError.new(
510+
"The following relations are not compatible with alternative activities: ",
511+
not_handled_relations
512+
)
513+
end
514+
487515
def check_routes(periodic_heuristic)
488516
@hash[:routes]&.each{ |route|
489517
route[:mission_ids].each{ |id|

models/relation.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
module Models
2121
class Relation < Base
2222
ALL_OR_NONE_RELATIONS = %i[shipment meetup].freeze
23+
ALTERNATIVE_COMPATIBLE_RELATIONS = %i[
24+
order
25+
same_route
26+
sequence
27+
shipment
28+
force_first
29+
never_first
30+
force_end
31+
].freeze
2332

2433
# Relations that link multiple services to be on the same route
2534
LINKING_RELATIONS = %i[

models/solution/parsers/stop_parser.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
module Parsers
2121
class ServiceParser
2222
def self.parse(service, options)
23-
activity = options[:index] && service.activities[options[:index]] || service.activity
23+
alternative_index = options[:index] || (service.activity ? 0 : (service.activities.size - 1))
24+
activity = service.activity || service.activities[alternative_index]
2425
activity_hash = Models::Activity.field_names.map{ |key|
2526
next if key == :point_id
2627

@@ -36,7 +37,7 @@ def self.parse(service, options)
3637
pickup_shipment_id: service.type == :pickup ? (service.original_id || service.id) : nil,
3738
delivery_shipment_id: service.type == :delivery ? (service.original_id || service.id) : nil,
3839
type: service.type,
39-
alternative: options[:index],
40+
alternative: options[:index], # nil if unassigned but return by default the last activity
4041
loads: build_loads(service, options),
4142
activity: dup_activity,
4243
info: options[:info] || Models::Solution::Stop::Info.new({}),

test/lib/interpreters/split_clustering_test.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,16 @@ def test_which_relations_are_linking_and_forcing
768768
minimum_duration_lapse
769769
vehicle_trips
770770
], Models::Relation::FORCING_RELATIONS, 'Forcing relation constant has changed'
771+
772+
assert_equal %i[
773+
force_end
774+
force_first
775+
never_first
776+
order
777+
same_route
778+
sequence
779+
shipment
780+
], Models::Relation::ALTERNATIVE_COMPATIBLE_RELATIONS.sort, 'Forcing relation constant has changed'
771781
end
772782

773783
def test_collect_data_items_respects_linking_relations

test/test_helper.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,70 @@ def self.basic
406406
}
407407
end
408408

409+
def self.basic_alternatives
410+
{
411+
units: [{ id: 'kg' }],
412+
matrices: [{
413+
id: 'matrix_0',
414+
time: [
415+
[0, 4, 5, 5],
416+
[6, 0, 1, 5],
417+
[1, 2, 0, 5],
418+
[5, 5, 5, 0]
419+
]
420+
}],
421+
points: [{
422+
id: 'point_0',
423+
matrix_index: 0
424+
}, {
425+
id: 'point_1',
426+
matrix_index: 1
427+
}, {
428+
id: 'point_2',
429+
matrix_index: 2
430+
}, {
431+
id: 'point_3',
432+
matrix_index: 3
433+
}],
434+
vehicles: [{
435+
id: 'vehicle_0',
436+
matrix_id: 'matrix_0',
437+
start_point_id: 'point_0'
438+
}],
439+
services: [{
440+
id: 'service_1',
441+
activities: [{
442+
point_id: 'point_1',
443+
}, {
444+
point_id: 'point_2'
445+
}]
446+
}, {
447+
id: 'service_2',
448+
activities: [{
449+
point_id: 'point_2'
450+
}, {
451+
point_id: 'point_3'
452+
}]
453+
}, {
454+
id: 'service_3',
455+
activities: [{
456+
point_id: 'point_3'
457+
}, {
458+
point_id: 'point_1'
459+
}]
460+
}],
461+
configuration: {
462+
resolution: {
463+
duration: 100
464+
},
465+
preprocessing: {},
466+
restitution: {
467+
intermediate_solutions: false,
468+
}
469+
}
470+
}
471+
end
472+
409473
def self.pud
410474
{
411475
matrices: [{

test/wrappers/ortools_test.rb

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4372,14 +4372,13 @@ def test_no_solver_with_ortools_single_heuristic
43724372

43734373
def test_insert_with_order
43744374
vrp = TestHelper.load_vrp(self, fixture_file: 'instance_order')
4375+
ordered_service_ids = vrp.relations.first.linked_service_ids
43754376
solutions = OptimizerWrapper.wrapper_vrp('demo', { services: { vrp: [:ortools] }}, vrp, nil)
43764377
assert solutions[0]
43774378
assert_equal 0, solutions[0].unassigned_stops.size, 'All services should be planned.'
43784379

4379-
order_in_route = vrp[:relations][0][:linked_service_ids].collect{ |service_id|
4380-
solutions[0].routes.first.stops.find_index{ |activity| activity.service_id == service_id }
4381-
}
4382-
assert_equal order_in_route.sort, order_in_route, 'Services with order relation should appear in correct order in route.'
4380+
order_in_route = solutions[0].routes.first.stops.select{ |activity| ordered_service_ids.include?(activity.service_id) }.map(&:service_id)
4381+
assert_equal vrp.relations.first.linked_service_ids, order_in_route, 'Services with order relation should appear in correct order in route.'
43834382
end
43844383

43854384
def test_ordre_with_2_vehicles
@@ -5175,4 +5174,56 @@ def test_always_one_time_window_provided_for_rests
51755174
end
51765175
}
51775176
end
5177+
5178+
def test_activities_positions
5179+
problem = VRP.basic_alternatives
5180+
problem[:services].first[:activities].each{ |activity|
5181+
activity[:position] = :always_last
5182+
}
5183+
OptimizerWrapper.config[:services][:ortools].solve(TestHelper.create(problem), 'test')
5184+
end
5185+
5186+
def test_activities_build_unassigned
5187+
problem = VRP.basic_alternatives
5188+
vrp = Models::Vrp.create(problem)
5189+
5190+
OptimizerWrapper.config[:services][:ortools].stub(
5191+
:build_unassigned,
5192+
lambda { |_, _|
5193+
unassigned_services = vrp.services.map{ |service| [service.id, service] }.to_h
5194+
unassigned_rests = []
5195+
OptimizerWrapper.config[:services][:ortools].send(:__minitest_stub__build_unassigned, unassigned_services, unassigned_rests)
5196+
}
5197+
) do
5198+
OptimizerWrapper.config[:services][:ortools].solve(vrp, 'test')
5199+
end
5200+
end
5201+
5202+
def test_activities_order
5203+
problem = VRP.basic_alternatives
5204+
problem[:services][0][:activities][0][:timewindows] = [{start: 0, end: 20}]
5205+
problem[:services][0][:activities][1][:timewindows] = [{start: 100, end: 120}]
5206+
problem[:services][1][:activities][0][:timewindows] = [{start: 100, end: 120}]
5207+
problem[:services][1][:activities][1][:timewindows] = [{start: 0, end: 20}]
5208+
problem[:services][2][:activities][0][:timewindows] = [{start: 0, end: 20}]
5209+
problem[:services][2][:activities][1][:timewindows] = [{start: 100, end: 120}]
5210+
problem[:relations] = [{
5211+
type: :order,
5212+
linked_service_ids: ['service_1', 'service_2', 'service_3']
5213+
}]
5214+
5215+
solution = OptimizerWrapper.config[:services][:ortools].solve(TestHelper.create(problem), 'test')
5216+
alternative_indices = solution.routes[0].stops.map(&:alternative).compact.uniq
5217+
assert_equal 2, alternative_indices.size
5218+
assert_equal 1, (solution.routes[0].stops.index{ |stop| stop.id == 'service_1' })
5219+
assert_equal 2, (solution.routes[0].stops.index{ |stop| stop.id == 'service_2' })
5220+
assert_equal 3, (solution.routes[0].stops.index{ |stop| stop.id == 'service_3' })
5221+
5222+
problem[:services][1][:activities][0][:timewindows] = [{start: 0, end: 1}]
5223+
problem[:services][1][:activities][1][:timewindows] = [{start: 0, end: 1}]
5224+
5225+
solution = OptimizerWrapper.config[:services][:ortools].solve(TestHelper.create(problem), 'test')
5226+
unassigned_stops = solution.unassigned_stops
5227+
assert_equal 2, unassigned_stops.size
5228+
end
51785229
end

wrappers/ortools.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def solve(vrp, job, thread_proc = nil, &block)
7777
duplicated_begins = already_begin.uniq.select{ |linked_id| already_begin.select{ |link| link == linked_id }.size > 1 }
7878
already_end = order_relations.collect{ |relation| relation.linked_service_ids[1..-1] }.flatten
7979
duplicated_ends = already_end.uniq.select{ |linked_id| already_end.select{ |link| link == linked_id }.size > 1 }
80-
if vrp.routes.empty? && order_relations.size == 1
80+
if vrp.routes.empty? && order_relations.size == 1 && vrp.services.none?{ |service| service.activities.any? }
8181
order_relations.select{ |relation| (relation.linked_service_ids[0..-2] & duplicated_begins).size == 0 && (relation.linked_service_ids[1..-1] & duplicated_ends).size == 0 }.each{ |relation|
8282
order_route = {
8383
vehicle: (vrp.vehicles.size == 1) ? vrp.vehicles.first : nil,
@@ -185,7 +185,7 @@ def solve(vrp, job, thread_proc = nil, &block)
185185

186186
services = update_services_positions(services, services_positions, service.id, service.activity.position, service_index)
187187
elsif service.activities
188-
service.activities.each_with_index{ |possible_activity, activity_index|
188+
service.activities.each{ |possible_activity|
189189
services << OrtoolsVrp::Service.new(
190190
time_windows: possible_activity.timewindows.collect{ |tw|
191191
OrtoolsVrp::TimeWindow.new(start: tw.start, end: tw.end || 2147483647, maximum_lateness: tw.maximum_lateness)
@@ -209,7 +209,7 @@ def solve(vrp, job, thread_proc = nil, &block)
209209
matrix_index: possible_activity.point.matrix_index,
210210
vehicle_indices: vehicles_indices,
211211
setup_duration: possible_activity.setup_duration,
212-
id: "#{service.id}_activity#{activity_index}",
212+
id: service.id.to_s,
213213
late_multiplier: possible_activity.late_multiplier || 0,
214214
setup_quantities: vrp.units.collect{ |unit|
215215
q = service.quantities.find{ |quantity| quantity.unit == unit }
@@ -336,7 +336,6 @@ def solve(vrp, job, thread_proc = nil, &block)
336336
)
337337

338338
log "ortools solve problem creation elapsed: #{Time.now - tic}sec", level: :debug
339-
340339
run_ortools(problem, vrp, thread_proc, &block)
341340
end
342341

0 commit comments

Comments
 (0)