diff --git a/CHANGELOG.md b/CHANGELOG.md index b199acad2..5adfe988d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [v1.8.0-dev] - Unreleased ### Added +- Support skills in periodic heuristic (`first_solution_strategy='periodic'`) [#194](https://github.com/Mapotempo/optimizer-api/pull/194) ### Changed diff --git a/lib/heuristics/concerns/scheduling_data_initialisation.rb b/lib/heuristics/concerns/scheduling_data_initialisation.rb index 7e38afeb3..2b8e94658 100644 --- a/lib/heuristics/concerns/scheduling_data_initialisation.rb +++ b/lib/heuristics/concerns/scheduling_data_initialisation.rb @@ -26,6 +26,7 @@ def generate_route_structure(vrp) capacity = compute_capacities(vehicle[:capacities], true) vrp.units.reject{ |unit| capacity.has_key?(unit[:id]) }.each{ |unit| capacity[unit[:id]] = 0.0 } @candidate_routes[vehicle.original_id][vehicle.global_day_index] = { + vehicle: vehicle, vehicle_id: vehicle.id, global_day_index: vehicle.global_day_index, tw_start: (vehicle.timewindow.start < 84600) ? vehicle.timewindow.start : vehicle.timewindow.start - vehicle.global_day_index * 86400, diff --git a/lib/heuristics/scheduling_heuristic.rb b/lib/heuristics/scheduling_heuristic.rb index fafdb254d..4ac828a6b 100644 --- a/lib/heuristics/scheduling_heuristic.rb +++ b/lib/heuristics/scheduling_heuristic.rb @@ -842,8 +842,15 @@ def compatible_days(service_id, day) !@services_data[service_id][:raw].unavailable_days.include?(day) end + def compatible_vehicle(service_id, route_data) + # WARNING : this does not consider vehicle alternative skills properly + # we would need to know which skill_set is required in order that all services on same vehicle are compatible + route_data[:vehicle].skills.any?{ |skill_set| (@services_data[service_id][:raw].skills - skill_set).empty? } + end + def service_compatible_with_route(service_id, route_data) - compatible_days(service_id, route_data[:global_day_index]) + compatible_days(service_id, route_data[:global_day_index]) && + compatible_vehicle(service_id, route_data) end def find_best_index(service_id, route_data, first_visit = true) diff --git a/test/fixtures/add_missing_visits_candidate_routes.bindump b/test/fixtures/add_missing_visits_candidate_routes.bindump index 86fb19060..2ea80a34d 100644 Binary files a/test/fixtures/add_missing_visits_candidate_routes.bindump and b/test/fixtures/add_missing_visits_candidate_routes.bindump differ diff --git a/test/fixtures/reaffecting_without_allow_partial_assignment_routes.bindump b/test/fixtures/reaffecting_without_allow_partial_assignment_routes.bindump index c9d971aaf..28219429a 100644 Binary files a/test/fixtures/reaffecting_without_allow_partial_assignment_routes.bindump and b/test/fixtures/reaffecting_without_allow_partial_assignment_routes.bindump differ diff --git a/test/lib/heuristics/scheduling_instance_validity_test.rb b/test/lib/heuristics/scheduling_instance_validity_test.rb index 767367653..e6404aac4 100644 --- a/test/lib/heuristics/scheduling_instance_validity_test.rb +++ b/test/lib/heuristics/scheduling_instance_validity_test.rb @@ -64,14 +64,6 @@ def test_reject_if_vehicle_distance assert_includes OptimizerWrapper.config[:services][:ortools].inapplicable_solve?(TestHelper.create(problem)), :assert_no_vehicle_distance_if_heuristic end - def test_reject_if_vehicle_skills - problem = VRP.scheduling - problem[:vehicles].first[:skills] = [['skill']] - problem[:services].first[:skills] = ['skill'] - - assert_includes OptimizerWrapper.config[:services][:ortools].inapplicable_solve?(TestHelper.create(problem)), :assert_no_skills_if_heuristic - end - def test_reject_if_vehicle_free_approach_return problem = VRP.scheduling problem[:vehicles].first[:free_approach] = true diff --git a/test/lib/heuristics/scheduling_test.rb b/test/lib/heuristics/scheduling_test.rb index 9890f1b1d..ee38bded7 100644 --- a/test/lib/heuristics/scheduling_test.rb +++ b/test/lib/heuristics/scheduling_test.rb @@ -553,6 +553,20 @@ def test_sticky_in_scheduling refute_includes result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } }[:vehicle_id], 'vehicle_0_' end + def test_skills_in_scheduling + vrp = VRP.lat_lon_scheduling_two_vehicles + result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil) + assigned_route = result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } } + assert_includes assigned_route[:vehicle_id], 'vehicle_0_' # default result + + vrp = VRP.lat_lon_scheduling_two_vehicles + vrp[:vehicles][1][:skills] = [[:compatible]] + vrp[:services].find{ |s| s[:id] == 'service_6' }[:skills] = [:compatible] + result = OptimizerWrapper.wrapper_vrp('ortools', { services: { vrp: [:ortools] }}, TestHelper.create(vrp), nil) + assigned_route = result[:routes].find{ |r| r[:activities].any?{ |stop| stop[:service_id] == 'service_6_1_1' } } + refute_includes assigned_route[:vehicle_id], 'vehicle_0_' + end + def test_with_activities vrp = VRP.lat_lon_scheduling_two_vehicles vrp[:configuration][:resolution][:minimize_days_worked] = true diff --git a/wrappers/ortools.rb b/wrappers/ortools.rb index c69cab201..011c10672 100644 --- a/wrappers/ortools.rb +++ b/wrappers/ortools.rb @@ -38,6 +38,7 @@ def solver_constraints :assert_vehicles_objective, :assert_vehicles_no_capacity_initial, :assert_vehicles_no_alternative_skills, + :assert_vehicles_no_alternative_skills_with_periodic_heuristic, :assert_zones_only_size_one_alternative, :assert_only_empty_or_fill_quantities, :assert_points_same_definition, @@ -55,7 +56,6 @@ def solver_constraints :assert_no_vehicle_overall_duration_if_heuristic, :assert_no_vehicle_distance_if_heuristic, :assert_possible_to_get_distances_if_maximum_ride_distance, - :assert_no_skills_if_heuristic, :assert_no_vehicle_free_approach_or_return_if_heuristic, :assert_no_vehicle_limit_if_heuristic, :assert_no_same_point_day_if_no_heuristic, diff --git a/wrappers/wrapper.rb b/wrappers/wrapper.rb index dbc991abf..fe161a3a5 100644 --- a/wrappers/wrapper.rb +++ b/wrappers/wrapper.rb @@ -61,9 +61,12 @@ def assert_vehicles_no_capacity_initial(vrp) end def assert_vehicles_no_alternative_skills(vrp) - vrp.vehicles.empty? || vrp.vehicles.none?{ |vehicle| - !vehicle.skills || vehicle.skills.size > 1 - } + vrp.vehicles.none?{ |vehicle| vehicle.skills.size > 1 } + end + + def assert_vehicles_no_alternative_skills_with_periodic_heuristic(vrp) + !vrp.preprocessing_first_solution_strategy.include?('periodic') || + vrp.vehicles.none?{ |vehicle| vehicle.skills.size > 1 } end def assert_no_direct_shipments(vrp) @@ -295,10 +298,6 @@ def assert_possible_to_get_distances_if_maximum_ride_distance(vrp) vrp.vehicles.none?(&:maximum_ride_distance) || (vrp.points.all?{ |point| point.location&.lat } || vrp.matrices.all?{ |matrix| matrix.distance && !matrix.distance.empty? }) end - def assert_no_skills_if_heuristic(vrp) - vrp.services.none?{ |service| !service.skills.empty? } || vrp.vehicles.none?{ |vehicle| !vehicle.skills.flatten.empty? } || vrp.preprocessing_first_solution_strategy.to_a.first != 'periodic' || !vrp.preprocessing_partitions.empty? - end - def assert_no_vehicle_free_approach_or_return_if_heuristic(vrp) vrp.vehicles.none?{ |vehicle| vehicle.free_approach || vehicle.free_return } || vrp.preprocessing_first_solution_strategy.to_a.first != 'periodic' end