diff --git a/.bazelignore b/.bazelignore index 807b1738f8..3d8c9b42e3 100644 --- a/.bazelignore +++ b/.bazelignore @@ -4,3 +4,7 @@ install_make build_cross dependencies/install dependencies/sources +temp_cpp +temp_dotnet +temp_java +temp_python diff --git a/ortools/constraint_solver/constraint_solver.h b/ortools/constraint_solver/constraint_solver.h index 09f3ec9e8b..c5a69e37f5 100644 --- a/ortools/constraint_solver/constraint_solver.h +++ b/ortools/constraint_solver/constraint_solver.h @@ -1769,14 +1769,26 @@ class Solver { /// and the distance between nodes is one, the {force/distance} of nodes /// would be: start{0/1} P1{0/1} P2{1/1} D1{3/1} D2{2/1} end{0/0}. /// The energy would be 0*1 + 1*1 + 3*1 + 2*1 + 0*1. - /// The cost of each path is - /// costs[path] = energy[path] * path_unit_costs[path]. + /// The cost per unit of energy is cost_per_unit_below_threshold until the + /// force reaches the threshold, then it is cost_per_unit_above_threshold: + /// min(threshold, force.CumulVar(Next(node))) * distance.TransitVar(node) * + /// cost_per_unit_below_threshold + max(0, force.CumulVar(Next(node)) - + /// threshold) * distance.TransitVar(node) * cost_per_unit_above_threshold. struct PathEnergyCostConstraintSpecification { + struct EnergyCost { + int64_t threshold; + int64_t cost_per_unit_below_threshold; + int64_t cost_per_unit_above_threshold; + bool IsNull() const { + return (cost_per_unit_below_threshold == 0 || threshold == 0) && + (cost_per_unit_above_threshold == 0 || threshold == kint64max); + } + }; std::vector nexts; std::vector paths; std::vector forces; std::vector distances; - std::vector path_unit_costs; + std::vector path_energy_costs; std::vector path_used_when_empty; std::vector path_starts; std::vector path_ends; diff --git a/ortools/constraint_solver/diffn.cc b/ortools/constraint_solver/diffn.cc index 53fc5d96f6..88e02bc400 100644 --- a/ortools/constraint_solver/diffn.cc +++ b/ortools/constraint_solver/diffn.cc @@ -275,7 +275,7 @@ class Diffn : public Constraint { } Constraint* MakeCumulativeConstraint(const std::vector& positions, - const std::vector& sizes, + absl::Span sizes, const std::vector& demands, int64_t capacity) { std::vector intervals; diff --git a/ortools/constraint_solver/graph_constraints.cc b/ortools/constraint_solver/graph_constraints.cc index 2c99dac509..d5ab3d72dc 100644 --- a/ortools/constraint_solver/graph_constraints.cc +++ b/ortools/constraint_solver/graph_constraints.cc @@ -1661,7 +1661,7 @@ class PathEnergyCostConstraint : public Constraint { const int num_nexts = specification_.nexts.size(); DCHECK_EQ(num_nexts, specification_.distances.size()); - const int num_paths = specification_.path_unit_costs.size(); + const int num_paths = specification_.path_energy_costs.size(); DCHECK_EQ(num_paths, specification_.costs.size()); DCHECK_EQ(num_paths, specification_.path_used_when_empty.size()); DCHECK_EQ(num_paths, specification_.path_starts.size()); @@ -1685,11 +1685,13 @@ class PathEnergyCostConstraint : public Constraint { // after all per-node calls have triggered. void PathEnergyCostConstraint::Post() { // path demons are delayed, to be run after all node demons. - path_demons_.resize(specification_.path_unit_costs.size(), nullptr); - for (int path = 0; path < specification_.path_unit_costs.size(); ++path) { + const int num_paths = specification_.path_energy_costs.size(); + path_demons_.resize(num_paths, nullptr); + for (int path = 0; path < num_paths; ++path) { // If unit cost is 0, initial propagation takes care of setting the cost // to 0, no need to register a demon. - if (specification_.path_unit_costs[path] == 0) continue; + const auto& cost = specification_.path_energy_costs[path]; + if (cost.IsNull()) continue; path_demons_[path] = MakeDelayedConstraintDemon1( solver(), this, &PathEnergyCostConstraint::PropagatePath, "PropagatePath", path); @@ -1711,9 +1713,10 @@ void PathEnergyCostConstraint::Post() { } void PathEnergyCostConstraint::InitialPropagate() { - const int num_paths = specification_.path_unit_costs.size(); + const int num_paths = specification_.path_energy_costs.size(); for (int path = 0; path < num_paths; ++path) { - if (specification_.path_unit_costs[path] == 0) { + const auto& cost = specification_.path_energy_costs[path]; + if (cost.IsNull()) { specification_.costs[path]->SetValue(0); } else { PropagatePath(path); @@ -1733,8 +1736,12 @@ void PathEnergyCostConstraint::NodeDispatcher(int node) { // Propagate only forces, distance -> energy. void PathEnergyCostConstraint::PropagatePath(int path) { // Compute energy along mandatory path. - int64_t energy_min = 0; - int64_t energy_max = 0; + const auto& [threshold, cost_per_unit_below, cost_per_unit_above] = + specification_.path_energy_costs[path]; + int64_t energy_below_min = 0; + int64_t energy_below_max = 0; + int64_t energy_above_min = 0; + int64_t energy_above_max = 0; int current = specification_.path_starts[path]; const int num_nodes = specification_.nexts.size(); int num_nonend_nodes = 0; @@ -1746,22 +1753,28 @@ void PathEnergyCostConstraint::PropagatePath(int path) { const int64_t distance_min = specification_.distances[current]->Min(); const int64_t distance_max = specification_.distances[current]->Max(); DCHECK_GE(distance_min, 0); // Bounds are correct when distance is >= 0. - energy_min = CapAdd( - energy_min, CapProd(specification_.forces[next]->Min(), distance_min)); - energy_max = CapAdd( - energy_max, CapProd(specification_.forces[next]->Max(), distance_max)); + const IntVar* force = specification_.forces[next]; + CapAddTo(CapProd(std::min(threshold, force->Min()), distance_min), + &energy_below_min); + CapAddTo(CapProd(std::min(threshold, force->Max()), distance_max), + &energy_below_max); + CapAddTo(CapProd(std::max(0, CapSub(force->Min(), threshold)), + distance_min), + &energy_above_min); + CapAddTo(CapProd(std::max(0, CapSub(force->Max(), threshold)), + distance_max), + &energy_above_max); current = next; } - // TODO(user): test more incremental propagation. Energy can be computed - // as unordered sum of force[next] * distance[node], it can be done without - // browsing the whole path at every trigger. if (current == specification_.path_ends[path]) { if (num_nonend_nodes == 1 && !specification_.path_used_when_empty[path]) { specification_.costs[path]->SetValue(0); } else { specification_.costs[path]->SetRange( - CapProd(energy_min, specification_.path_unit_costs[path]), - CapProd(energy_max, specification_.path_unit_costs[path])); + CapAdd(CapProd(energy_below_min, cost_per_unit_below), + CapProd(energy_above_min, cost_per_unit_above)), + CapAdd(CapProd(energy_below_max, cost_per_unit_below), + CapProd(energy_above_max, cost_per_unit_above))); } } } diff --git a/ortools/constraint_solver/java/RoutingSolverTest.java b/ortools/constraint_solver/java/RoutingSolverTest.java index 1d3a704408..717c49e8f4 100644 --- a/ortools/constraint_solver/java/RoutingSolverTest.java +++ b/ortools/constraint_solver/java/RoutingSolverTest.java @@ -23,6 +23,7 @@ import com.google.auto.value.AutoValue; import com.google.ortools.Loader; import com.google.ortools.constraintsolver.RoutingModelParameters; import com.google.ortools.constraintsolver.RoutingSearchParameters; +import com.google.ortools.constraintsolver.RoutingSearchStatus; import com.google.protobuf.Duration; import java.util.ArrayList; import java.util.function.LongBinaryOperator; @@ -170,7 +171,7 @@ public final class RoutingSolverTest { System.gc(); model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); Assignment solution = model.solveWithParameters(parameters); assertEquals(10, solution.objectiveValue()); solution = model.solveFromAssignmentWithParameters(solution, parameters); @@ -189,9 +190,9 @@ public final class RoutingSolverTest { System.gc(); model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(10, solution.objectiveValue()); } @@ -214,9 +215,9 @@ public final class RoutingSolverTest { System.gc(); // model should keep alive the callback model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(10, solution.objectiveValue()); } @@ -232,9 +233,9 @@ public final class RoutingSolverTest { System.gc(); // model should keep alive the callback model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(10, solution.objectiveValue()); } @@ -254,9 +255,9 @@ public final class RoutingSolverTest { System.gc(); // model should keep alive the callback model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(8, solution.objectiveValue()); } @@ -273,9 +274,9 @@ public final class RoutingSolverTest { System.gc(); // model should keep alive the callback model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(45, solution.objectiveValue()); } @@ -291,9 +292,9 @@ public final class RoutingSolverTest { System.gc(); // model should keep alive the callback model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(8, solution.objectiveValue()); } @@ -312,9 +313,9 @@ public final class RoutingSolverTest { System.gc(); // model should keep alive the callback model.setArcCostEvaluatorOfAllVehicles(cost); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(10, solution.objectiveValue()); } @@ -387,9 +388,9 @@ public final class RoutingSolverTest { .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solveWithParameters(searchParameters); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(20, solution.objectiveValue()); } @@ -416,9 +417,9 @@ public final class RoutingSolverTest { .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solveWithParameters(searchParameters); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(45, solution.objectiveValue()); } @@ -451,9 +452,9 @@ public final class RoutingSolverTest { .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solveWithParameters(searchParameters); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(10, solution.objectiveValue()); } @@ -500,9 +501,9 @@ public final class RoutingSolverTest { .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); Assignment solution = model.solveWithParameters(searchParameters); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); solution = model.solve(solution); assertNotNull(solution); @@ -527,9 +528,9 @@ public final class RoutingSolverTest { dimension.setSpanCostCoefficientForAllVehicles(2); assertEquals(2, dimension.getSpanCostCoefficientForVehicle(0)); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_OPTIMAL, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_OPTIMAL, model.status()); assertNotNull(solution); assertEquals(2 * (10 + 1), solution.objectiveValue()); } @@ -551,9 +552,9 @@ public final class RoutingSolverTest { timeDimension.setGlobalSpanCostCoefficient(2); assertEquals(2, timeDimension.getGlobalSpanCostCoefficient()); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_SUCCESS, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_SUCCESS, model.status()); assertNotNull(solution); assertEquals(2 * (11 - 1), solution.objectiveValue()); } @@ -587,9 +588,9 @@ public final class RoutingSolverTest { model.registerTransitCallback(callback), 1000, capacity, false, "dim"); RoutingDimension dimension = model.getMutableDimension("dim"); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_OPTIMAL, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_OPTIMAL, model.status()); assertNotNull(solution); for (int vehicle = 0; vehicle < 3; ++vehicle) { assertEquals(vehicle + 4, solution.max(dimension.cumulVar(model.start(vehicle)))); @@ -614,9 +615,9 @@ public final class RoutingSolverTest { model.addDimensionWithVehicleTransits(transits, 1000, capacity, false, "dim"); RoutingDimension dimension = model.getMutableDimension("dim"); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_OPTIMAL, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_OPTIMAL, model.status()); assertNotNull(solution); for (int vehicle = 0; vehicle < 3; ++vehicle) { assertEquals( @@ -645,9 +646,9 @@ public final class RoutingSolverTest { model.addDimensionWithVehicleTransitAndCapacity(transits, 1000, capacity, false, "dim"); final RoutingDimension dimension = model.getMutableDimension("dim"); - assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_NOT_SOLVED, model.status()); final Assignment solution = model.solve(null); - assertEquals(RoutingModel.ROUTING_OPTIMAL, model.status()); + assertEquals(RoutingSearchStatus.Value.ROUTING_OPTIMAL, model.status()); assertNotNull(solution); for (int vehicle = 0; vehicle < 3; ++vehicle) { assertEquals(4, solution.max(dimension.cumulVar(model.start(vehicle)))); diff --git a/ortools/constraint_solver/java/routing.i b/ortools/constraint_solver/java/routing.i index 12ccd26126..4d8bc3137e 100644 --- a/ortools/constraint_solver/java/routing.i +++ b/ortools/constraint_solver/java/routing.i @@ -28,10 +28,16 @@ namespace operations_research { class RoutingModelParameters; class RoutingSearchParameters; + +struct RoutingSearchStatus { + enum Value {}; +}; + } // namespace operations_research // Include the files we want to wrap a first time. %{ +#include "ortools/constraint_solver/routing_enums.pb.h" #include "ortools/constraint_solver/routing_types.h" #include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/constraint_solver/routing_parameters.h" @@ -227,6 +233,7 @@ import java.util.function.LongUnaryOperator; %rename (solveFromAssignmentWithParameters) RoutingModel::SolveFromAssignmentWithParameters; %rename (solveWithParameters) RoutingModel::SolveWithParameters; %rename (start) RoutingModel::Start; +%rename (status) RoutingModel::status; %rename (unperformedPenalty) RoutingModel::UnperformedPenalty; %rename (unperformedPenaltyOrValue) RoutingModel::UnperformedPenaltyOrValue; %rename (vehicleVar) RoutingModel::VehicleVar; @@ -370,6 +377,8 @@ PROTO2_RETURN(operations_research::RoutingSearchParameters, com.google.ortools.constraintsolver.RoutingSearchParameters) PROTO2_RETURN(operations_research::RoutingModelParameters, com.google.ortools.constraintsolver.RoutingModelParameters) +PROTO_ENUM_RETURN(operations_research::RoutingSearchStatus::Value, + com.google.ortools.constraintsolver.RoutingSearchStatus.Value) // Wrap routing_types.h, routing_parameters.h according to the SWIG styleguide. %ignoreall diff --git a/ortools/constraint_solver/local_search.cc b/ortools/constraint_solver/local_search.cc index 0fbaa42dcd..85743a88e3 100644 --- a/ortools/constraint_solver/local_search.cc +++ b/ortools/constraint_solver/local_search.cc @@ -4397,7 +4397,7 @@ class LocalSearchProfiler : public LocalSearchMonitor { "First solution statistics:\n%*s | Time (s)\n", max_name_size, ""); ParseFirstSolutionStatistics( - [&overview, max_name_size](const std::string& name, + [&overview, max_name_size](absl::string_view name, double duration_seconds) { absl::StrAppendFormat(&overview, "%*s | %7.2g\n", max_name_size, name, duration_seconds); diff --git a/ortools/constraint_solver/pack.cc b/ortools/constraint_solver/pack.cc index 6a18f08b5c..a8f5c9c3d2 100644 --- a/ortools/constraint_solver/pack.cc +++ b/ortools/constraint_solver/pack.cc @@ -23,6 +23,7 @@ #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" +#include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" @@ -522,7 +523,7 @@ void SortWeightVector(std::vector* const indices, } void SortIndexByWeight(std::vector* const indices, - const std::vector& weights) { + absl::Span weights) { std::vector to_sort; for (int index = 0; index < indices->size(); ++index) { if (weights[index] != 0) { diff --git a/ortools/constraint_solver/python/pywraprouting_test.py b/ortools/constraint_solver/python/pywraprouting_test.py index ec689ac49f..6e163a746d 100755 --- a/ortools/constraint_solver/python/pywraprouting_test.py +++ b/ortools/constraint_solver/python/pywraprouting_test.py @@ -101,9 +101,13 @@ class TestPyWrapRoutingModel(absltest.TestCase): self.assertIsNotNone(manager) model = pywrapcp.RoutingModel(manager) self.assertIsNotNone(model) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() - self.assertEqual(model.ROUTING_OPTIMAL, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_OPTIMAL, model.status() + ) self.assertIsNotNone(assignment) self.assertEqual(0, assignment.ObjectiveValue()) @@ -112,9 +116,13 @@ class TestPyWrapRoutingModel(absltest.TestCase): self.assertIsNotNone(manager) model = pywrapcp.RoutingModel(manager) self.assertIsNotNone(model) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() - self.assertEqual(model.ROUTING_OPTIMAL, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_OPTIMAL, model.status() + ) self.assertIsNotNone(assignment) self.assertEqual(0, assignment.ObjectiveValue()) @@ -128,10 +136,14 @@ class TestPyWrapRoutingModel(absltest.TestCase): ) self.assertEqual(1, transit_idx) model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual(pywrapcp.RoutingModel.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() self.assertTrue(assignment) - self.assertEqual(pywrapcp.RoutingModel.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(20, assignment.ObjectiveValue()) def testTransitLambda(self): @@ -142,9 +154,13 @@ class TestPyWrapRoutingModel(absltest.TestCase): transit_id = model.RegisterTransitCallback(lambda from_index, to_index: 1) self.assertEqual(1, transit_id) model.SetArcCostEvaluatorOfAllVehicles(transit_id) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertIsNotNone(assignment) self.assertEqual(5, assignment.ObjectiveValue()) @@ -157,10 +173,14 @@ class TestPyWrapRoutingModel(absltest.TestCase): transit_idx = model.RegisterTransitMatrix(matrix) self.assertEqual(1, transit_idx) model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() self.assertTrue(assignment) - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(15, assignment.ObjectiveValue()) def testUnaryTransitCallback(self): @@ -173,10 +193,14 @@ class TestPyWrapRoutingModel(absltest.TestCase): ) self.assertEqual(1, transit_idx) model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() self.assertTrue(assignment) - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(10, assignment.ObjectiveValue()) def testUnaryTransitLambda(self): @@ -187,9 +211,13 @@ class TestPyWrapRoutingModel(absltest.TestCase): transit_id = model.RegisterUnaryTransitCallback(lambda from_index: 1) self.assertEqual(1, transit_id) model.SetArcCostEvaluatorOfAllVehicles(transit_id) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertIsNotNone(assignment) self.assertEqual(5, assignment.ObjectiveValue()) @@ -202,10 +230,14 @@ class TestPyWrapRoutingModel(absltest.TestCase): transit_idx = model.RegisterUnaryTransitVector(vector) self.assertEqual(1, transit_idx) model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.Solve() self.assertTrue(assignment) - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(45, assignment.ObjectiveValue()) def testTSP(self): @@ -219,14 +251,18 @@ class TestPyWrapRoutingModel(absltest.TestCase): functools.partial(TransitDistance, manager) ) model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) # Solve search_parameters = pywrapcp.DefaultRoutingSearchParameters() search_parameters.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE ) assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(90, assignment.ObjectiveValue()) # Inspect solution index = model.Start(0) @@ -451,10 +487,14 @@ class TestPyWrapRoutingModel(absltest.TestCase): search_parameters.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE ) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.SolveWithParameters(search_parameters) self.assertIsNotNone(assignment) - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(90, assignment.ObjectiveValue()) # Inspect solution node = model.Start(0) @@ -486,10 +526,14 @@ class TestPyWrapRoutingModel(absltest.TestCase): search_parameters.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE ) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.SolveWithParameters(search_parameters) self.assertIsNotNone(assignment) - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(20, assignment.ObjectiveValue()) # Inspect solution index = model.Start(0) @@ -520,10 +564,14 @@ class TestPyWrapRoutingModel(absltest.TestCase): search_parameters.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE ) - self.assertEqual(model.ROUTING_NOT_SOLVED, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) assignment = model.SolveWithParameters(search_parameters) self.assertIsNotNone(assignment) - self.assertEqual(model.ROUTING_SUCCESS, model.status()) + self.assertEqual( + routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) self.assertEqual(20, assignment.ObjectiveValue()) # Inspect solution for v in range(manager.GetNumberOfVehicles()): diff --git a/ortools/constraint_solver/python/routing.i b/ortools/constraint_solver/python/routing.i index 61ed18d7be..c5f7b19c8c 100644 --- a/ortools/constraint_solver/python/routing.i +++ b/ortools/constraint_solver/python/routing.i @@ -28,10 +28,12 @@ namespace operations_research { class RoutingModelParameters; class RoutingSearchParameters; +class RoutingSearchStatus; } // namespace operations_research // Include the files we want to wrap a first time. %{ +#include "ortools/constraint_solver/routing_enums.pb.h" #include "ortools/constraint_solver/routing_types.h" #include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/constraint_solver/routing_parameters.h" @@ -101,6 +103,10 @@ struct LocalSearchMetaheuristic { enum Value {}; }; +struct RoutingSearchStatus { + enum Value {}; +}; + // SimpleBoundCosts %unignore BoundCost; diff --git a/ortools/constraint_solver/routing.cc b/ortools/constraint_solver/routing.cc index 9ef00d55b2..5419e6bc6c 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/constraint_solver/routing.cc @@ -48,6 +48,7 @@ #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/span.h" +#include "google/protobuf/util/message_differencer.h" #include "ortools/base/dump_vars.h" #include "ortools/base/int_type.h" #include "ortools/base/logging.h" @@ -411,6 +412,7 @@ RoutingModel::RoutingModel(const RoutingIndexManager& index_manager, num_visit_types_(0), paths_metadata_(index_manager), manager_(index_manager), + search_parameters_(DefaultRoutingSearchParameters()), finalizer_variables_(std::make_unique(solver_.get())), interrupt_cp_sat_(false), interrupt_cp_(false) { @@ -1065,7 +1067,8 @@ void ResourceGroup::ComputeResourceClasses() { std::vector& assignable_to_v = resource_class.assignable_to_vehicle; assignable_to_v.resize(model_->vehicles_, false); for (int v : vehicles_requiring_resource_) { - assignable_to_v[v] = IsResourceAllowedForVehicle(r, v); + assignable_to_v[v] = IsResourceAllowedForVehicle(r, v) && + model_->ResourceVar(v, index_)->Contains(r); } DCHECK_EQ(resource_indices_per_class_.size(), resource_class_map.size()); @@ -1173,19 +1176,36 @@ void RoutingModel::SetFixedCostOfVehicle(int64_t cost, int vehicle) { void RoutingModel::SetPathEnergyCostOfVehicle(const std::string& force, const std::string& distance, - int64_t unit_cost, int vehicle) { + int64_t cost_per_unit, + int vehicle) { + SetPathEnergyCostsOfVehicle(force, distance, /*threshold=*/0, + /*cost_per_unit_below_threshold=*/0, + /*cost_per_unit_above_threshold=*/cost_per_unit, + vehicle); +} + +void RoutingModel::SetPathEnergyCostsOfVehicle( + const std::string& force, const std::string& distance, int64_t threshold, + int64_t cost_per_unit_below_threshold, + int64_t cost_per_unit_above_threshold, int vehicle) { DCHECK_LE(0, vehicle); DCHECK_LT(vehicle, vehicles_); - DCHECK_LE(0, unit_cost); - // When unit_cost is 0, the path energy cost is 0, we can avoid useless - // computations. - if (unit_cost == 0) return; - std::vector& vehicle_unit_costs = - force_distance_to_vehicle_unit_costs_[std::make_pair(force, distance)]; - if (vehicle_unit_costs.size() < vehicles_) { - vehicle_unit_costs.resize(vehicles_, 0); + DCHECK_LE(0, threshold); + DCHECK_LE(0, cost_per_unit_below_threshold); + DCHECK_LE(0, cost_per_unit_above_threshold); + // When costs are 0, we can avoid useless computations. + if (cost_per_unit_below_threshold == 0 && cost_per_unit_above_threshold == 0) + return; + using Limit = Solver::PathEnergyCostConstraintSpecification::EnergyCost; + std::vector& energy_costs = + force_distance_to_energy_costs_[std::make_pair(force, distance)]; + if (energy_costs.size() < vehicles_) { + energy_costs.resize(vehicles_, {0, 0, 0}); } - vehicle_unit_costs[vehicle] = unit_cost; + energy_costs[vehicle] = { + .threshold = threshold, + .cost_per_unit_below_threshold = cost_per_unit_below_threshold, + .cost_per_unit_above_threshold = cost_per_unit_above_threshold}; } void RoutingModel::SetAmortizedCostFactorsOfAllVehicles( @@ -2030,7 +2050,7 @@ class RoutingModelInspector : public ModelVisitor { void VisitIntegerArrayArgument(const std::string& arg_name, const std::vector& values) override { gtl::FindWithDefault(array_inspectors_, arg_name, - [](const std::vector&) {})(values); + [](absl::Span) {})(values); } private: @@ -2170,9 +2190,10 @@ absl::Duration GetTimeLimit(const RoutingSearchParameters& parameters) { void RoutingModel::CloseModelWithParameters( const RoutingSearchParameters& parameters) { - std::string error = FindErrorInRoutingSearchParameters(parameters); + status_ = RoutingSearchStatus::ROUTING_NOT_SOLVED; + const std::string error = FindErrorInRoutingSearchParameters(parameters); if (!error.empty()) { - status_ = ROUTING_INVALID; + status_ = RoutingSearchStatus::ROUTING_INVALID; LOG(ERROR) << "Invalid RoutingSearchParameters: " << error; return; } @@ -2510,18 +2531,16 @@ void RoutingModel::CloseModelWithParameters( cost_elements.push_back(CreateSameVehicleCost(i)); } // Energy costs - { - for (const auto& [force_distance, vehicle_unit_costs] : - force_distance_to_vehicle_unit_costs_) { - std::vector vehicle_costs(vehicles_, nullptr); + for (const auto& [force_distance, costs] : force_distance_to_energy_costs_) { + std::vector energy_costs(vehicles_, nullptr); - for (int vehicle = 0; vehicle < vehicles_; ++vehicle) { - const int64_t cost_ub = - vehicle_unit_costs[vehicle] == 0 ? 0 : kint64max; - vehicle_costs[vehicle] = solver_->MakeIntVar(0, cost_ub); - cost_elements.push_back(vehicle_costs[vehicle]); - AddWeightedVariableMinimizedByFinalizer(vehicle_costs[vehicle], - vehicle_unit_costs[vehicle]); + for (int v = 0; v < vehicles_; ++v) { + const int64_t cost_ub = costs[v].IsNull() ? 0 : kint64max; + energy_costs[v] = solver_->MakeIntVar(0, cost_ub); + cost_elements.push_back(energy_costs[v]); + AddWeightedVariableMinimizedByFinalizer( + energy_costs[v], std::max(costs[v].cost_per_unit_below_threshold, + costs[v].cost_per_unit_above_threshold)); } const RoutingDimension* force_dimension = @@ -2537,16 +2556,15 @@ void RoutingModel::CloseModelWithParameters( .paths = VehicleVars(), .forces = force_dimension->cumuls(), .distances = distance_dimension->transits(), - .path_unit_costs = vehicle_unit_costs, + .path_energy_costs = costs, .path_used_when_empty = vehicle_used_when_empty_, .path_starts = paths_metadata_.Starts(), .path_ends = paths_metadata_.Ends(), - .costs = vehicle_costs, + .costs = energy_costs, }; solver_->AddConstraint( solver_->MakePathEnergyCostConstraint(specification)); - } } // cost_ is the sum of cost_elements. cost_ = solver_->MakeSum(cost_elements)->Var(); @@ -2645,12 +2663,10 @@ void RoutingModel::CloseModelWithParameters( // parameters. CreateNeighborhoodOperators(parameters); CreateFirstSolutionDecisionBuilders(parameters); - error = FindErrorInSearchParametersForModel(parameters); - if (!error.empty()) { - status_ = ROUTING_INVALID; - LOG(ERROR) << "Invalid RoutingSearchParameters for this model: " << error; - return; - } + monitors_before_setup_ = monitors_.size(); + // This must set here as SetupSearch needs to be aware of previously existing + // monitors. + monitors_after_setup_ = monitors_.size(); SetupSearch(parameters); } @@ -2790,8 +2806,10 @@ const Assignment* RoutingModel::FastSolveFromAssignmentWithParameters( } const int64_t start_time_ms = solver_->wall_time(); QuietCloseModelWithParameters(search_parameters); - if (status_ == ROUTING_INVALID) return nullptr; - status_ = ROUTING_NOT_SOLVED; + UpdateSearchFromParametersIfNeeded(search_parameters); + + if (status_ == RoutingSearchStatus::ROUTING_INVALID) return nullptr; + status_ = RoutingSearchStatus::ROUTING_NOT_SOLVED; if (assignment == nullptr) return nullptr; limit_->UpdateLimits( GetTimeLimit(search_parameters), std::numeric_limits::max(), @@ -2810,14 +2828,14 @@ const Assignment* RoutingModel::FastSolveFromAssignmentWithParameters( if (!check_solution_in_cp || CheckIfAssignmentIsFeasible(*solution, /*call_at_solution_monitors=*/false)) { - status_ = ROUTING_SUCCESS; + status_ = RoutingSearchStatus::ROUTING_SUCCESS; } } - if (status_ != ROUTING_SUCCESS) { + if (status_ != RoutingSearchStatus::ROUTING_SUCCESS) { if (elapsed_time >= GetTimeLimit(search_parameters)) { - status_ = ROUTING_FAIL_TIMEOUT; + status_ = RoutingSearchStatus::ROUTING_FAIL_TIMEOUT; } else { - status_ = ROUTING_FAIL; + status_ = RoutingSearchStatus::ROUTING_FAIL; } } return solution; @@ -2829,16 +2847,16 @@ const Assignment* RoutingModel::SolveFromAssignmentsWithParameters( std::vector* solutions) { const int64_t start_time_ms = solver_->wall_time(); QuietCloseModelWithParameters(parameters); - VLOG(1) << "Search parameters:\n" << parameters; + UpdateSearchFromParametersIfNeeded(parameters); if (solutions != nullptr) solutions->clear(); - if (status_ == ROUTING_INVALID) { + if (status_ == RoutingSearchStatus::ROUTING_INVALID) { return nullptr; } - status_ = ROUTING_NOT_SOLVED; + status_ = RoutingSearchStatus::ROUTING_NOT_SOLVED; // Detect infeasibilities at the root of the search tree. if (!solver_->CheckConstraint(solver_->MakeTrueConstraint())) { - status_ = ROUTING_INFEASIBLE; + status_ = RoutingSearchStatus::ROUTING_INFEASIBLE; return nullptr; } @@ -2872,7 +2890,7 @@ const Assignment* RoutingModel::SolveFromAssignmentsWithParameters( return true; }; if (!update_time_limits()) { - status_ = ROUTING_FAIL_TIMEOUT; + status_ = RoutingSearchStatus::ROUTING_FAIL_TIMEOUT; return nullptr; } lns_limit_->UpdateLimits( @@ -2988,8 +3006,9 @@ const Assignment* RoutingModel::SolveFromAssignmentsWithParameters( if (solution_collector->has_solution() || !solution_pool.empty()) { status_ = local_optimum_reached_ - ? ROUTING_SUCCESS - : ROUTING_PARTIAL_SUCCESS_LOCAL_OPTIMUM_NOT_REACHED; + ? RoutingSearchStatus::ROUTING_SUCCESS + : RoutingSearchStatus:: + ROUTING_PARTIAL_SUCCESS_LOCAL_OPTIMUM_NOT_REACHED; if (solutions != nullptr) { std::vector temp_solutions; for (int i = 0; i < solution_collector->solution_count(); ++i) { @@ -3033,7 +3052,7 @@ const Assignment* RoutingModel::SolveFromAssignmentsWithParameters( solutions->insert(solutions->end(), temp_solutions.begin(), temp_solutions.end()); if (min_objective_value <= objective_lower_bound_) { - status_ = ROUTING_OPTIMAL; + status_ = RoutingSearchStatus::ROUTING_OPTIMAL; } return solutions->back(); } @@ -3045,14 +3064,14 @@ const Assignment* RoutingModel::SolveFromAssignmentsWithParameters( } } if (best_assignment->ObjectiveValue() <= objective_lower_bound_) { - status_ = ROUTING_OPTIMAL; + status_ = RoutingSearchStatus::ROUTING_OPTIMAL; } return solver_->MakeAssignment(best_assignment); } else { if (elapsed_time >= GetTimeLimit(parameters)) { - status_ = ROUTING_FAIL_TIMEOUT; + status_ = RoutingSearchStatus::ROUTING_FAIL_TIMEOUT; } else { - status_ = ROUTING_FAIL; + status_ = RoutingSearchStatus::ROUTING_FAIL; } return nullptr; } @@ -3062,23 +3081,30 @@ const Assignment* RoutingModel::SolveWithIteratedLocalSearch( const RoutingSearchParameters& parameters) { const int64_t start_time_ms = solver_->wall_time(); QuietCloseModelWithParameters(parameters); + UpdateSearchFromParametersIfNeeded(parameters); + if (status_ == RoutingSearchStatus::ROUTING_INVALID) { + return nullptr; + } // Build an initial solution. solver_->Solve(solve_db_, monitors_); - uint64_t explored_solutions = solver_->solutions(); + int64_t explored_solutions = solver_->solutions(); Assignment* best_solution = collect_assignments_->last_solution_or_null(); if (!best_solution) { return nullptr; } + // The solution that tracks the search trajectory. + Assignment* last_accepted_solution = solver_->MakeAssignment(best_solution); + LocalSearchFilterManager* filter_manager = GetOrCreateLocalSearchFilterManager(parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/false}); DecisionBuilder* perturbation_db = MakePerturbationDecisionBuilder( - parameters, this, best_solution, + parameters, this, last_accepted_solution, [this]() { return CheckLimit(time_buffer_); }, filter_manager); // TODO(user): This lambda can probably be refactored into a function as a @@ -3114,27 +3140,37 @@ const Assignment* RoutingModel::SolveWithIteratedLocalSearch( solver_->Solve(perturbation_db, monitors_); explored_solutions += solver_->solutions(); - Assignment* neighbor = collect_assignments_->last_solution_or_null(); - if (!neighbor) { + Assignment* neighbor_solution = + collect_assignments_->last_solution_or_null(); + if (!neighbor_solution) { continue; } - if (improve_perturbed_solution) { - assignment_->CopyIntersection(neighbor); + if (improve_perturbed_solution && update_time_limits()) { + assignment_->CopyIntersection(neighbor_solution); solver_->Solve(improve_db_, monitors_); + explored_solutions += solver_->solutions(); - neighbor = collect_assignments_->last_solution_or_null(); - if (!neighbor) { + neighbor_solution = collect_assignments_->last_solution_or_null(); + if (!neighbor_solution) { continue; } } - if (acceptance_criterion->Accept(best_solution, neighbor)) { - // Note that the ruin_and_recreate_db is using best_solution as reference - // assignment. By updating best_solution here we thus also keep the - // ruin_and_recreate_db reference assignment up to date. - best_solution->CopyIntersection(neighbor); + if (neighbor_solution->ObjectiveValue() < best_solution->ObjectiveValue()) { + best_solution->CopyIntersection(neighbor_solution); + } + + absl::Duration elapsed_time = + absl::Milliseconds(solver_->wall_time() - start_time_ms); + if (acceptance_criterion->Accept({elapsed_time, explored_solutions}, + neighbor_solution, + last_accepted_solution)) { + // Note that the perturbation_db is using last_accepted_solution as + // reference assignment. By updating last_accepted_solution here we thus + // also keep the perturbation_db reference assignment up to date. + last_accepted_solution->CopyIntersection(neighbor_solution); } } @@ -3483,15 +3519,15 @@ Assignment* RoutingModel::RestoreAssignment(const Assignment& solution) { } Assignment* RoutingModel::DoRestoreAssignment() { - if (status_ == ROUTING_INVALID) { + if (status_ == RoutingSearchStatus::ROUTING_INVALID) { return nullptr; } solver_->Solve(restore_assignment_, monitors_); if (collect_assignments_->solution_count() == 1) { - status_ = ROUTING_SUCCESS; + status_ = RoutingSearchStatus::ROUTING_SUCCESS; return collect_assignments_->solution(0); } else { - status_ = ROUTING_FAIL; + status_ = RoutingSearchStatus::ROUTING_FAIL; return nullptr; } return nullptr; @@ -3785,9 +3821,9 @@ int64_t RoutingModel::GetDimensionTransitCostSum( cost_class.dimension_transit_evaluator_class_and_cost_coefficient) { DCHECK_GE(span_cost_coefficient, 0); if (span_cost_coefficient == 0) continue; - cost = CapAdd(cost, CapProd(span_cost_coefficient, - dimension->GetTransitValueFromClass( - i, j, transit_evaluator_class))); + CapAddTo(CapProd(span_cost_coefficient, dimension->GetTransitValueFromClass( + i, j, transit_evaluator_class)), + &cost); } return cost; } @@ -4740,15 +4776,19 @@ RoutingModel::CreateLocalSearchFilters( } if (options.filter_objective) { const int num_vehicles = vehicles(); - for (const auto& [force_distance, unit_costs] : - force_distance_to_vehicle_unit_costs_) { + for (const auto& [force_distance, energy_costs] : + force_distance_to_energy_costs_) { const auto& [force, distance] = force_distance; const RoutingDimension* force_dimension = GetMutableDimension(force); DCHECK_NE(force_dimension, nullptr); if (force_dimension == nullptr) continue; + std::vector force_start_min(num_vehicles); + std::vector force_end_min(num_vehicles); std::vector force_class(num_vehicles); std::vector*> force_evaluators; for (int v = 0; v < num_vehicles; ++v) { + force_start_min[v] = force_dimension->GetCumulVarMin(Start(v)); + force_end_min[v] = force_dimension->GetCumulVarMin(End(v)); const int c = force_dimension->vehicle_to_class(v); force_class[v] = c; if (c >= force_evaluators.size()) { @@ -4777,10 +4817,22 @@ RoutingModel::CreateLocalSearchFilters( &(distance_dimension->GetBinaryTransitEvaluator(v)); } } + std::vector path_energy_costs; + for (const auto& limit : energy_costs) { + path_energy_costs.push_back({ + .threshold = limit.threshold, + .cost_per_unit_below_threshold = + limit.cost_per_unit_below_threshold, + .cost_per_unit_above_threshold = + limit.cost_per_unit_above_threshold, + }); + } auto checker = std::make_unique( - path_state_reference, std::move(force_class), + path_state_reference, std::move(force_start_min), + std::move(force_end_min), std::move(force_class), std::move(force_evaluators), std::move(distance_class), - std::move(distance_evaluators), unit_costs, vehicle_used_when_empty_); + std::move(distance_evaluators), std::move(path_energy_costs), + vehicle_used_when_empty_); filter_events.push_back( {MakePathEnergyCostFilter(solver(), std::move(checker), absl::StrCat(force_dimension->name(), @@ -5405,41 +5457,41 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( {/*filter_objective=*/false, /*filter_with_cp_solver=*/false}); } - if (search_parameters.savings_parallel_routes()) { - IntVarFilteredDecisionBuilder* savings_db = + IntVarFilteredDecisionBuilder* parallel_savings_db = CreateIntVarFilteredDecisionBuilder( savings_parameters, filter_manager); if (!search_parameters.use_unfiltered_first_solution_strategy()) { first_solution_filtered_decision_builders_ - [FirstSolutionStrategy::SAVINGS] = savings_db; + [FirstSolutionStrategy::PARALLEL_SAVINGS] = parallel_savings_db; } - first_solution_decision_builders_[FirstSolutionStrategy::SAVINGS] = - solver_->Try(savings_db, CreateIntVarFilteredDecisionBuilder< - ParallelSavingsFilteredHeuristic>( + first_solution_decision_builders_[FirstSolutionStrategy::PARALLEL_SAVINGS] = + solver_->Try( + parallel_savings_db, + CreateIntVarFilteredDecisionBuilder( savings_parameters, GetOrCreateLocalSearchFilterManager( - search_parameters, - {/*filter_objective=*/false, + search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/true}))); - } else { - IntVarFilteredDecisionBuilder* savings_db = + + IntVarFilteredDecisionBuilder* sequential_savings_db = CreateIntVarFilteredDecisionBuilder( savings_parameters, filter_manager); if (!search_parameters.use_unfiltered_first_solution_strategy()) { - first_solution_filtered_decision_builders_ - [FirstSolutionStrategy::SAVINGS] = savings_db; + first_solution_filtered_decision_builders_[FirstSolutionStrategy::SAVINGS] = + sequential_savings_db; } first_solution_decision_builders_[FirstSolutionStrategy::SAVINGS] = - solver_->Try(savings_db, CreateIntVarFilteredDecisionBuilder< + solver_->Try( + sequential_savings_db, + CreateIntVarFilteredDecisionBuilder< SequentialSavingsFilteredHeuristic>( savings_parameters, GetOrCreateLocalSearchFilterManager( - search_parameters, - {/*filter_objective=*/false, + search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/true}))); - } + // Sweep first_solution_decision_builders_[FirstSolutionStrategy::SWEEP] = MakeSweepDecisionBuilder(this, true); @@ -5813,6 +5865,11 @@ SearchMonitor* MakeLocalOptimumWatcher( void RoutingModel::SetupSearchMonitors( const RoutingSearchParameters& search_parameters) { + std::vector old_monitors = monitors_; + monitors_.clear(); + for (int i = 0; i < monitors_before_setup_; ++i) { + monitors_.push_back(old_monitors[i]); + } monitors_.push_back(GetOrCreateLimit()); monitors_.push_back(MakeLocalOptimumWatcher( solver(), @@ -5830,6 +5887,11 @@ void RoutingModel::SetupSearchMonitors( SetupMetaheuristics(search_parameters); SetupAssignmentCollector(search_parameters); SetupTrace(search_parameters); + int new_monitors_after_setup = monitors_.size(); + for (int i = monitors_after_setup_; i < old_monitors.size(); ++i) { + monitors_.push_back(old_monitors[i]); + } + monitors_after_setup_ = new_monitors_after_setup; } bool RoutingModel::UsesLightPropagation( @@ -5870,8 +5932,34 @@ void RoutingModel::AddVariableMinimizedByFinalizer(IntVar* var) { void RoutingModel::SetupSearch( const RoutingSearchParameters& search_parameters) { + const std::string error = + FindErrorInSearchParametersForModel(search_parameters); + if (!error.empty()) { + status_ = RoutingSearchStatus::ROUTING_INVALID; + LOG(ERROR) << "Invalid RoutingSearchParameters for this model: " << error; + return; + } SetupDecisionBuilders(search_parameters); SetupSearchMonitors(search_parameters); + search_parameters_ = search_parameters; +} + +void RoutingModel::UpdateSearchFromParametersIfNeeded( + const RoutingSearchParameters& search_parameters) { + // TODO(user): Cache old configs instead of overwriting them. This will + // avoid consuming extra memory for configs that were already considered. + if (!google::protobuf::util::MessageDifferencer::Equivalent( + search_parameters_, search_parameters)) { + status_ = RoutingSearchStatus::ROUTING_NOT_SOLVED; + std::string error = FindErrorInRoutingSearchParameters(search_parameters); + if (!error.empty()) { + status_ = RoutingSearchStatus::ROUTING_INVALID; + LOG(ERROR) << "Invalid RoutingSearchParameters: " << error; + } else { + SetupSearch(search_parameters); + } + } + VLOG(1) << "Search parameters:\n" << search_parameters; } void RoutingModel::AddToAssignment(IntVar* const var) { diff --git a/ortools/constraint_solver/routing.h b/ortools/constraint_solver/routing.h index 131aa6adad..41db282da6 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/constraint_solver/routing.h @@ -243,6 +243,8 @@ class PathsMetadata { int NumPaths() const { return start_of_path_.size(); } const std::vector& Paths() const { return path_of_node_; } const std::vector& Starts() const { return start_of_path_; } + int64_t Start(int path) const { return start_of_path_[path]; } + int64_t End(int path) const { return end_of_path_[path]; } const std::vector& Ends() const { return end_of_path_; } private: @@ -255,28 +257,6 @@ class PathsMetadata { class RoutingModel { public: - /// Status of the search. - enum Status { - /// Problem not solved yet (before calling RoutingModel::Solve()). - ROUTING_NOT_SOLVED, - /// Problem solved successfully after calling RoutingModel::Solve(). - ROUTING_SUCCESS, - /// Problem solved successfully after calling RoutingModel::Solve(), except - /// that a local optimum has not been reached. Leaving more time would allow - /// improving the solution. - ROUTING_PARTIAL_SUCCESS_LOCAL_OPTIMUM_NOT_REACHED, - /// No solution found to the problem after calling RoutingModel::Solve(). - ROUTING_FAIL, - /// Time limit reached before finding a solution with RoutingModel::Solve(). - ROUTING_FAIL_TIMEOUT, - /// Model, model parameters or flags are not valid. - ROUTING_INVALID, - /// Problem proven to be infeasible. - ROUTING_INFEASIBLE, - /// Problem has been solved to optimality. - ROUTING_OPTIMAL - }; - /// Types of precedence policy applied to pickup and delivery pairs. enum PickupAndDeliveryPolicy { /// Any precedence is accepted. @@ -549,6 +529,14 @@ class RoutingModel { DCHECK_LT(resource_index, resource_class_indices_.size()); return resource_class_indices_[resource_index]; } + const Attributes& GetDimensionAttributesForClass( + const RoutingDimension* dimension, ResourceClassIndex rc_index) const { + DCHECK_LT(rc_index, resource_indices_per_class_.size()); + const std::vector& resource_indices = + resource_indices_per_class_[rc_index]; + DCHECK(!resource_indices.empty()); + return resources_[resource_indices[0]].GetDimensionAttributes(dimension); + } int Size() const { return resources_.size(); } int Index() const { return index_; } @@ -1200,14 +1188,27 @@ class RoutingModel { /// than the first and last nodes. int64_t GetFixedCostOfVehicle(int vehicle) const; // Sets the energy cost of a vehicle. - // The energy used by a vehicle is the integral of the force dimension over - // the distance dimension: it is the sum over nodes visited by the vehicle of + // The energy used by a vehicle is defined as the integral of the + // force dimension over the distance dimension: + // it is the sum over nodes visited by the vehicle of // force.CumulVar(Next(node)) * distance.TransitVar(node). // The energy cost of a vehicle is linear in the energy used by the vehicle, - // this call sets the coefficient to unit_cost, it is zero if unset. + // this call sets the coefficient to cost_per_unit. The cost is zero if unset. void SetPathEnergyCostOfVehicle(const std::string& force, const std::string& distance, - int64_t unit_cost, int vehicle); + int64_t cost_per_unit, int vehicle); + // Sets the energy cost of a vehicle, relative to a threshold. + // The cost per unit of energy is cost_per_unit_below_threshold until the + // force reaches the threshold, then it is cost_per_unit_above_threshold: + // min(threshold, force.CumulVar(Next(node))) * distance.TransitVar(node) * + // cost_per_unit_below_threshold + max(0, force.CumulVar(Next(node)) - + // threshold) * distance.TransitVar(node) * cost_per_unit_above_threshold. + void SetPathEnergyCostsOfVehicle(const std::string& force, + const std::string& distance, + int64_t threshold, + int64_t cost_per_unit_below_threshold, + int64_t cost_per_unit_above_threshold, + int vehicle); /// The following methods set the linear and quadratic cost factors of /// vehicles (must be positive values). The default value of these parameters @@ -1366,7 +1367,7 @@ class RoutingModel { /// search. int64_t objective_lower_bound() const { return objective_lower_bound_; } /// Returns the current status of the routing model. - Status status() const { return status_; } + RoutingSearchStatus::Value status() const { return status_; } /// Returns the value of the internal enable_deep_serialization_ parameter. bool enable_deep_serialization() const { return enable_deep_serialization_; } /// Applies a lock chain to the next search. 'locks' represents an ordered @@ -2145,6 +2146,9 @@ class RoutingModel { const RoutingSearchParameters& search_parameters) const; /// Sets up search objects, such as decision builders and monitors. void SetupSearch(const RoutingSearchParameters& search_parameters); + /// Updates search objects if parameters have changed. + void UpdateSearchFromParametersIfNeeded( + const RoutingSearchParameters& search_parameters); /// Set of auxiliary methods used to setup the search. // TODO(user): Document each auxiliary method. Assignment* GetOrCreateAssignment(); @@ -2402,9 +2406,11 @@ class RoutingModel { /// considered for constraints. std::vector vehicle_used_when_empty_; #ifndef SWIG - absl::flat_hash_map, std::vector, + absl::flat_hash_map< + std::pair, + std::vector, absl::Hash>> - force_distance_to_vehicle_unit_costs_; + force_distance_to_energy_costs_; absl::StrongVector cost_classes_; #endif // SWIG bool costs_are_homogeneous_across_vehicles_; @@ -2499,7 +2505,7 @@ class RoutingModel { int start_end_count_; // Model status bool closed_ = false; - Status status_ = ROUTING_NOT_SOLVED; + RoutingSearchStatus::Value status_ = RoutingSearchStatus::ROUTING_NOT_SOLVED; bool enable_deep_serialization_ = true; // Secondary routing solver @@ -2518,6 +2524,8 @@ class RoutingModel { std::vector monitors_; std::vector secondary_ls_monitors_; std::vector at_solution_monitors_; + int monitors_before_setup_ = 0; + int monitors_after_setup_ = 0; SearchMonitor* metaheuristic_ = nullptr; SearchMonitor* search_log_ = nullptr; bool local_optimum_reached_ = false; @@ -2527,6 +2535,7 @@ class RoutingModel { SolutionCollector* collect_secondary_ls_assignments_ = nullptr; SolutionCollector* collect_one_assignment_ = nullptr; SolutionCollector* optimized_dimensions_assignment_collector_ = nullptr; + RoutingSearchParameters search_parameters_; DecisionBuilder* solve_db_ = nullptr; DecisionBuilder* improve_db_ = nullptr; DecisionBuilder* secondary_ls_db_ = nullptr; @@ -3064,6 +3073,19 @@ class RoutingDimension { } IntVar* SlackVar(int64_t index) const { return slacks_[index]; } + /// Some functions to allow users to use the interface without knowing about + /// the underlying CP model. + // TODO(user): Routing should not store its data in a CP model. + + /// Restricts the range of the cumul variable associated to index. + void SetCumulVarRange(int64_t index, int64_t min, int64_t max) { + CumulVar(index)->SetRange(min, max); + } + /// Gets the current minimum of the cumul variable associated to index. + int64_t GetCumulVarMin(int64_t index) const { return CumulVar(index)->Min(); } + /// Gets the current maximum of the cumul variable associated to index. + int64_t GetCumulVarMax(int64_t index) const { return CumulVar(index)->Max(); } + #if !defined(SWIGPYTHON) /// Like CumulVar(), TransitVar(), SlackVar() but return the whole variable /// vectors instead (indexed by int64_t var index). diff --git a/ortools/constraint_solver/routing_breaks.cc b/ortools/constraint_solver/routing_breaks.cc index b3090e1b64..a530fd846b 100644 --- a/ortools/constraint_solver/routing_breaks.cc +++ b/ortools/constraint_solver/routing_breaks.cc @@ -21,6 +21,7 @@ #include #include +#include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/base/types.h" @@ -1004,7 +1005,8 @@ class VehicleBreaksFilter : public BasePathFilter { VehicleBreaksFilter::VehicleBreaksFilter(const RoutingModel& routing_model, const RoutingDimension& dimension) : BasePathFilter(routing_model.Nexts(), - routing_model.Size() + routing_model.vehicles()), + routing_model.Size() + routing_model.vehicles(), + routing_model.GetPathsMetadata()), model_(routing_model), dimension_(dimension) { DCHECK(dimension_.HasBreakConstraints()); diff --git a/ortools/constraint_solver/routing_decision_builders.cc b/ortools/constraint_solver/routing_decision_builders.cc index 8adf36d8ae..135d9ce13a 100644 --- a/ortools/constraint_solver/routing_decision_builders.cc +++ b/ortools/constraint_solver/routing_decision_builders.cc @@ -140,7 +140,7 @@ bool DimensionFixedTransitsEqualTransitEvaluators( // corresponding 'variables' vector. void AppendRouteCumulAndBreakVarAndValues( const RoutingDimension& dimension, int vehicle, - const std::vector& cumul_values, + absl::Span cumul_values, absl::Span break_values, std::vector* variables, std::vector* values) { auto& vars = *variables; @@ -494,7 +494,8 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { monitor_(monitor), optimize_and_pack_(optimize_and_pack), dimension_travel_info_per_route_( - std::move(dimension_travel_info_per_route)) { + std::move(dimension_travel_info_per_route)), + decision_level_(0) { DCHECK(dimension_travel_info_per_route_.empty() || dimension_travel_info_per_route_.size() == global_optimizer_->dimension()->model()->vehicles()); @@ -526,6 +527,24 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { } Decision* Next(Solver* solver) override { + if (decision_level_.Value() == 2) return nullptr; + if (decision_level_.Value() == 1) { + Decision* d = set_values_from_targets_->Next(solver); + if (d == nullptr) decision_level_.SetValue(solver, 2); + return d; + } + decision_level_.SetValue(solver, 1); + if (!FillCPValues()) { + solver->Fail(); + } + set_values_from_targets_ = + MakeSetValuesFromTargets(solver, cp_variables_, cp_values_); + return solver->MakeAssignVariablesValuesOrDoNothing(cp_variables_, + cp_values_); + } + + private: + bool FillCPValues() { const RoutingDimension* dimension = global_optimizer_->dimension(); DCHECK(DimensionFixedTransitsEqualTransitEvaluators(*dimension)); RoutingModel* const model = dimension->model(); @@ -539,7 +558,7 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { &resource_indices_per_group_); if (status == DimensionSchedulingStatus::INFEASIBLE) { - solver->Fail(); + return false; } else if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { // If relaxation is not feasible, try the MILP optimizer. const DimensionSchedulingStatus mp_status = @@ -547,7 +566,7 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { global_mp_optimizer_, &cumul_values_, &break_start_end_values_, &resource_indices_per_group_); if (mp_status != DimensionSchedulingStatus::OPTIMAL) { - solver->Fail(); + return false; } } else { DCHECK(status == DimensionSchedulingStatus::OPTIMAL); @@ -588,15 +607,9 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { cp_values_[j] = cp_variables_[j]->Min(); } } - if (!solver->SolveAndCommit(MakeSetValuesFromTargets(solver, cp_variables_, - std::move(cp_values_)), - monitor_)) { - solver->Fail(); - } - return nullptr; + return true; } - private: DimensionSchedulingStatus ComputeCumulBreakAndResourceValues( GlobalDimensionCumulOptimizer* optimizer, std::vector* cumul_values, @@ -621,15 +634,20 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { GlobalDimensionCumulOptimizer* const global_mp_optimizer_; SearchMonitor* const monitor_; const bool optimize_and_pack_; - // The following 5 members are stored internally to avoid unnecessary memory + std::vector cp_variables_; + std::vector cp_values_; + // The following 3 members are stored internally to avoid unnecessary memory // reallocations. std::vector cumul_values_; std::vector break_start_end_values_; std::vector> resource_indices_per_group_; - std::vector cp_values_; - std::vector cp_variables_; const std::vector dimension_travel_info_per_route_; + // Decision level of this decision builder: + // - level 0: set remaining dimension values at once. + // - level 1: set remaining dimension values one by one. + Rev decision_level_; + DecisionBuilder* set_values_from_targets_ = nullptr; }; } // namespace diff --git a/ortools/constraint_solver/routing_enums.proto b/ortools/constraint_solver/routing_enums.proto index 0f1557bcb0..216b01c12e 100644 --- a/ortools/constraint_solver/routing_enums.proto +++ b/ortools/constraint_solver/routing_enums.proto @@ -50,6 +50,11 @@ message FirstSolutionStrategy { // "Scheduling of Vehicles from a Central Depot to a Number of Delivery // Points", Operations Research, Vol. 12, 1964, pp. 568-581 SAVINGS = 10; + // Parallel version of the Savings algorithm. + // Instead of extending a single route until it is no longer possible, + // the parallel version iteratively considers the next most improving + // feasible saving and possibly builds several routes in parallel. + PARALLEL_SAVINGS = 17; // Sweep algorithm (Wren & Holliday). // Reference: Anthony Wren & Alan Holliday: Computer Scheduling of Vehicles // from One or More Depots to a Number of Delivery Points Operational @@ -140,3 +145,27 @@ message LocalSearchMetaheuristic { GENERIC_TABU_SEARCH = 5; } } + +// Used by `RoutingModel` to report the status of the search for a solution. +message RoutingSearchStatus { + enum Value { + // Problem not solved yet (before calling RoutingModel::Solve()). + ROUTING_NOT_SOLVED = 0; + // Problem solved successfully after calling RoutingModel::Solve(). + ROUTING_SUCCESS = 1; + // Problem solved successfully after calling RoutingModel::Solve(), except + // that a local optimum has not been reached. Leaving more time would allow + // improving the solution. + ROUTING_PARTIAL_SUCCESS_LOCAL_OPTIMUM_NOT_REACHED = 2; + // No solution found to the problem after calling RoutingModel::Solve(). + ROUTING_FAIL = 3; + // Time limit reached before finding a solution with RoutingModel::Solve(). + ROUTING_FAIL_TIMEOUT = 4; + // Model, model parameters or flags are not valid. + ROUTING_INVALID = 5; + // Problem proven to be infeasible. + ROUTING_INFEASIBLE = 6; + // Problem has been solved to optimality. + ROUTING_OPTIMAL = 7; + } +} diff --git a/ortools/constraint_solver/routing_filters.cc b/ortools/constraint_solver/routing_filters.cc index 3e8ea5829a..e98f46131e 100644 --- a/ortools/constraint_solver/routing_filters.cc +++ b/ortools/constraint_solver/routing_filters.cc @@ -37,6 +37,7 @@ #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" +#include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/small_map.h" #include "ortools/base/strong_vector.h" @@ -206,8 +207,7 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter { } else if (current_inactive_nodes <= max_inactive_cardinality) { // Add penalty if there were not too many inactive nodes before the // move. - accepted_objective_value_ = - CapAdd(accepted_objective_value_, penalty); + CapAddTo(penalty, &accepted_objective_value_); } } else if (current_inactive_nodes > max_inactive_cardinality) { // Remove penalty if there were too many inactive nodes before the @@ -251,8 +251,7 @@ class NodeDisjunctionFilter : public IntVarLocalSearchFilter { if (inactive_per_disjunction_[i] > disjunction_indices.size() - max_cardinality && penalty > 0) { - synchronized_objective_value_ = - CapAdd(synchronized_objective_value_, penalty); + CapAddTo(penalty, &synchronized_objective_value_); } } } @@ -279,15 +278,16 @@ IntVarLocalSearchFilter* MakeNodeDisjunctionFilter( const int64_t BasePathFilter::kUnassigned = -1; BasePathFilter::BasePathFilter(const std::vector& nexts, - int next_domain_size) + int next_domain_size, + const PathsMetadata& paths_metadata) : IntVarLocalSearchFilter(nexts), + paths_metadata_(paths_metadata), node_path_starts_(next_domain_size, kUnassigned), - paths_(nexts.size(), -1), new_synchronized_unperformed_nodes_(nexts.size()), new_nexts_(nexts.size(), kUnassigned), touched_paths_(nexts.size()), touched_path_chain_start_ends_(nexts.size(), {kUnassigned, kUnassigned}), - ranks_(next_domain_size, -1), + ranks_(next_domain_size, kUnassigned), status_(BasePathFilter::UNKNOWN), lns_detected_(false) {} @@ -319,12 +319,14 @@ bool BasePathFilter::Accept(const Assignment* delta, touched_paths_.Set(start); int64_t& chain_start = touched_path_chain_start_ends_[start].first; - if (chain_start == kUnassigned || ranks_[index] < ranks_[chain_start]) { + if (chain_start == kUnassigned || paths_metadata_.IsStart(index) || + ranks_[index] < ranks_[chain_start]) { chain_start = index; } int64_t& chain_end = touched_path_chain_start_ends_[start].second; - if (chain_end == kUnassigned || ranks_[index] > ranks_[chain_end]) { + if (chain_end == kUnassigned || paths_metadata_.IsEnd(index) || + ranks_[index] > ranks_[chain_end]) { chain_end = index; } }; @@ -382,31 +384,7 @@ void BasePathFilter::ComputePathStarts(std::vector* path_starts, } } -bool BasePathFilter::HavePathsChanged() { - std::vector path_starts; - std::vector index_to_path(Size(), kUnassigned); - ComputePathStarts(&path_starts, &index_to_path); - if (path_starts.size() != starts_.size()) { - return true; - } - for (int i = 0; i < path_starts.size(); ++i) { - if (path_starts[i] != starts_[i]) { - return true; - } - } - for (int i = 0; i < Size(); ++i) { - if (index_to_path[i] != paths_[i]) { - return true; - } - } - return false; -} - void BasePathFilter::SynchronizeFullAssignment() { - // Subclasses of BasePathFilter might not propagate injected objective values - // so making sure it is done here (can be done again by the subclass if - // needed). - ComputePathStarts(&starts_, &paths_); for (int64_t index = 0; index < Size(); index++) { if (IsVarSynced(index) && Value(index) == index && node_path_starts_[index] != kUnassigned) { @@ -418,18 +396,20 @@ void BasePathFilter::SynchronizeFullAssignment() { node_path_starts_.assign(node_path_starts_.size(), kUnassigned); // Marking nodes on a path and storing next values. const int nexts_size = Size(); - for (const int64_t start : starts_) { - int node = start; - node_path_starts_[node] = start; - DCHECK(IsVarSynced(node)); - int next = Value(node); + for (int path = 0; path < NumPaths(); ++path) { + const int64_t start = Start(path); + node_path_starts_[start] = start; + if (IsVarSynced(start)) { + int64_t next = Value(start); while (next < nexts_size) { - node = next; + int64_t node = next; node_path_starts_[node] = start; DCHECK(IsVarSynced(node)); next = Value(node); } node_path_starts_[next] = start; + } + node_path_starts_[End(path)] = start; } for (const int touched : delta_touched_) { new_nexts_[touched] = kUnassigned; @@ -447,16 +427,11 @@ void BasePathFilter::OnSynchronize(const Assignment* delta) { } if (IsDisabled()) return; new_synchronized_unperformed_nodes_.ClearAll(); - if (delta == nullptr || delta->Empty() || starts_.empty()) { + if (delta == nullptr || delta->Empty() || + absl::c_all_of(ranks_, [](int rank) { return rank == kUnassigned; })) { SynchronizeFullAssignment(); return; } - // Subclasses of BasePathFilter might not propagate injected objective values - // so making sure it is done here (can be done again by the subclass if - // needed). - // This code supposes that path starts didn't change. - DCHECK(!absl::GetFlag(FLAGS_routing_strong_debug_checks) || - !HavePathsChanged()); const Assignment::IntContainer& container = delta->IntVarContainer(); touched_paths_.SparseClearAll(); for (const IntVarElement& new_element : container.elements()) { @@ -495,8 +470,10 @@ void BasePathFilter::OnSynchronize(const Assignment* delta) { void BasePathFilter::UpdateAllRanks() { ranks_.assign(ranks_.size(), kUnassigned); for (int r = 0; r < NumPaths(); ++r) { - UpdatePathRanksFromStart(Start(r)); - OnSynchronizePathFromStart(Start(r)); + const int64_t start = Start(r); + if (!IsVarSynced(start)) continue; + UpdatePathRanksFromStart(start); + OnSynchronizePathFromStart(start); } } @@ -549,7 +526,8 @@ class VehicleAmortizedCostFilter : public BasePathFilter { VehicleAmortizedCostFilter::VehicleAmortizedCostFilter( const RoutingModel& routing_model) : BasePathFilter(routing_model.Nexts(), - routing_model.Size() + routing_model.vehicles()), + routing_model.Size() + routing_model.vehicles(), + routing_model.GetPathsMetadata()), current_vehicle_cost_(0), delta_vehicle_cost_(0), current_route_lengths_(Size(), -1), @@ -581,7 +559,9 @@ void VehicleAmortizedCostFilter::OnAfterSynchronizePaths() { for (int vehicle = 0; vehicle < vehicle_to_start_.size(); vehicle++) { const int64_t start = vehicle_to_start_[vehicle]; DCHECK_EQ(vehicle, start_to_vehicle_[start]); - + if (!IsVarSynced(start)) { + return; + } const int route_length = current_route_lengths_[start]; DCHECK_GE(route_length, 0); @@ -595,8 +575,8 @@ void VehicleAmortizedCostFilter::OnAfterSynchronizePaths() { CapProd(quadratic_cost_factor_of_vehicle_[vehicle], route_length * route_length); - current_vehicle_cost_ = CapAdd( - current_vehicle_cost_, CapSub(linear_cost_factor, route_length_cost)); + CapAddTo(CapSub(linear_cost_factor, route_length_cost), + ¤t_vehicle_cost_); } } @@ -632,8 +612,7 @@ bool VehicleAmortizedCostFilter::AcceptPath(int64_t path_start, if (previous_route_length == 0) { // The route was empty before, it is no longer the case (changed path). CHECK_GT(new_route_length, 0); - delta_vehicle_cost_ = - CapAdd(delta_vehicle_cost_, linear_cost_factor_of_vehicle_[vehicle]); + CapAddTo(linear_cost_factor_of_vehicle_[vehicle], &delta_vehicle_cost_); } else if (new_route_length == 0) { // The route is now empty. delta_vehicle_cost_ = @@ -643,10 +622,9 @@ bool VehicleAmortizedCostFilter::AcceptPath(int64_t path_start, // Update the cost related to the sum of the squares of the route lengths. const int64_t quadratic_cost_factor = quadratic_cost_factor_of_vehicle_[vehicle]; - delta_vehicle_cost_ = - CapAdd(delta_vehicle_cost_, - CapProd(quadratic_cost_factor, - previous_route_length * previous_route_length)); + CapAddTo(CapProd(quadratic_cost_factor, + previous_route_length * previous_route_length), + &delta_vehicle_cost_); delta_vehicle_cost_ = CapSub( delta_vehicle_cost_, CapProd(quadratic_cost_factor, new_route_length * new_route_length)); @@ -694,7 +672,8 @@ class TypeRegulationsFilter : public BasePathFilter { }; TypeRegulationsFilter::TypeRegulationsFilter(const RoutingModel& model) - : BasePathFilter(model.Nexts(), model.Size() + model.vehicles()), + : BasePathFilter(model.Nexts(), model.Size() + model.vehicles(), + model.GetPathsMetadata()), routing_model_(model), start_to_vehicle_(model.Size(), -1), temporal_incompatibility_checker_(model, @@ -770,6 +749,7 @@ bool TypeRegulationsFilter::HardIncompatibilitiesRespected(int vehicle, // Update new_type_counts by decrementing the occurrence of the types of the // nodes no longer on the route. + if (IsVarSynced(chain_start)) { node = Value(chain_start); while (node != chain_end) { const int type = routing_model_.GetVisitType(node); @@ -783,6 +763,7 @@ bool TypeRegulationsFilter::HardIncompatibilitiesRespected(int vehicle, } node = Value(node); } + } // Check the incompatibilities for types in types_to_check. for (int type : types_to_check) { @@ -852,7 +833,8 @@ class ChainCumulFilter : public BasePathFilter { ChainCumulFilter::ChainCumulFilter(const RoutingModel& routing_model, const RoutingDimension& dimension) - : BasePathFilter(routing_model.Nexts(), dimension.cumuls().size()), + : BasePathFilter(routing_model.Nexts(), dimension.cumuls().size(), + routing_model.GetPathsMetadata()), cumuls_(dimension.cumuls()), evaluators_(routing_model.vehicles(), nullptr), vehicle_capacities_(dimension.vehicle_capacities()), @@ -888,7 +870,7 @@ void ChainCumulFilter::OnSynchronizePathFromStart(int64_t start) { old_vehicles_[node] = vehicle; current_transits_[node] = (*evaluators_[vehicle])(node, next); } - cumul = CapAdd(cumul, current_transits_[node]); + CapAddTo(current_transits_[node], &cumul); cumul = std::max(cumuls_[next]->Min(), cumul); node = next; } @@ -913,9 +895,9 @@ bool ChainCumulFilter::AcceptPath(int64_t path_start, int64_t chain_start, const int64_t next = GetNext(node); if (IsVarSynced(node) && next == Value(node) && vehicle == old_vehicles_[node]) { - cumul = CapAdd(cumul, current_transits_[node]); + CapAddTo(current_transits_[node], &cumul); } else { - cumul = CapAdd(cumul, (*evaluators_[vehicle])(node, next)); + CapAddTo((*evaluators_[vehicle])(node, next), &cumul); } cumul = std::max(cumuls_[next]->Min(), cumul); if (cumul > capacity) return false; @@ -1223,7 +1205,8 @@ PathCumulFilter::PathCumulFilter(const RoutingModel& routing_model, const RoutingDimension& dimension, bool propagate_own_objective_value, bool filter_objective_cost, bool can_use_lp) - : BasePathFilter(routing_model.Nexts(), dimension.cumuls().size()), + : BasePathFilter(routing_model.Nexts(), dimension.cumuls().size(), + routing_model.GetPathsMetadata()), routing_model_(routing_model), dimension_(dimension), cumuls_(dimension.cumuls()), @@ -1387,8 +1370,8 @@ int64_t PathCumulFilter::GetPathCumulSoftLowerBoundCost( node = path_transits.Node(path, i); cumul = CapSub(cumul, path_transits.Transit(path, i)); cumul = std::min(cumuls_[node]->Max(), cumul); - current_cumul_cost_value = CapAdd(current_cumul_cost_value, - GetCumulSoftLowerBoundCost(node, cumul)); + CapAddTo(GetCumulSoftLowerBoundCost(node, cumul), + ¤t_cumul_cost_value); } return current_cumul_cost_value; } @@ -1397,7 +1380,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { total_current_cumul_cost_value_ = 0; cumul_cost_delta_ = 0; current_cumul_cost_values_.clear(); - if (NumPaths() > 0 && + if (HasAnySyncedPath() && (FilterSpanCost() || FilterCumulSoftBounds() || FilterSlackCost() || FilterCumulSoftLowerBounds() || FilterCumulPiecewiseLinearCosts() || FilterPrecedences() || FilterSoftSpanCost() || @@ -1411,6 +1394,9 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { // For each path, compute the minimum end cumul and store the max of these. for (int r = 0; r < NumPaths(); ++r) { int64_t node = Start(r); + if (!IsVarSynced(node)) { + continue; + } const int vehicle = start_to_vehicle_[Start(r)]; // First pass: evaluating route length to reserve memory to store route // information. @@ -1427,26 +1413,25 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { min_path_cumuls_.push_back(cumul); int64_t current_cumul_cost_value = GetCumulSoftCost(node, cumul); - current_cumul_cost_value = CapAdd( - current_cumul_cost_value, GetCumulPiecewiseLinearCost(node, cumul)); + CapAddTo(GetCumulPiecewiseLinearCost(node, cumul), + ¤t_cumul_cost_value); int64_t total_transit = 0; while (node < Size()) { const int64_t next = Value(node); const int64_t transit = (*evaluators_[vehicle])(node, next); - total_transit = CapAdd(total_transit, transit); + CapAddTo(transit, &total_transit); const int64_t transit_slack = CapAdd(transit, slacks_[node]->Min()); current_path_transits_.PushTransit(r, node, next, transit_slack); - cumul = CapAdd(cumul, transit_slack); + CapAddTo(transit_slack, &cumul); cumul = dimension_.GetFirstPossibleGreaterOrEqualValueForNode(next, cumul); cumul = std::max(cumuls_[next]->Min(), cumul); min_path_cumuls_.push_back(cumul); node = next; - current_cumul_cost_value = - CapAdd(current_cumul_cost_value, GetCumulSoftCost(node, cumul)); - current_cumul_cost_value = CapAdd( - current_cumul_cost_value, GetCumulPiecewiseLinearCost(node, cumul)); + CapAddTo(GetCumulSoftCost(node, cumul), ¤t_cumul_cost_value); + CapAddTo(GetCumulPiecewiseLinearCost(node, cumul), + ¤t_cumul_cost_value); } if (FilterPrecedences()) { StoreMinMaxCumulOfNodesOnPath(/*path=*/r, min_path_cumuls_, @@ -1466,10 +1451,9 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { current_path_transits_, r, Start(r), cumul); const int64_t span_lower_bound = CapSub(cumul, start); if (FilterSlackCost()) { - current_cumul_cost_value = - CapAdd(current_cumul_cost_value, - CapProd(vehicle_total_slack_cost_coefficients_[vehicle], - CapSub(span_lower_bound, total_transit))); + CapAddTo(CapProd(vehicle_total_slack_cost_coefficients_[vehicle], + CapSub(span_lower_bound, total_transit)), + ¤t_cumul_cost_value); } if (FilterSoftSpanCost()) { const BoundCost bound_cost = @@ -1477,8 +1461,8 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { if (bound_cost.bound < span_lower_bound) { const int64_t violation = CapSub(span_lower_bound, bound_cost.bound); - current_cumul_cost_value = CapAdd( - current_cumul_cost_value, CapProd(bound_cost.cost, violation)); + CapAddTo(CapProd(bound_cost.cost, violation), + ¤t_cumul_cost_value); } } if (FilterSoftSpanQuadraticCost()) { @@ -1487,16 +1471,14 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { if (bound_cost.bound < span_lower_bound) { const int64_t violation = CapSub(span_lower_bound, bound_cost.bound); - current_cumul_cost_value = - CapAdd(current_cumul_cost_value, - CapProd(bound_cost.cost, CapProd(violation, violation))); + CapAddTo(CapProd(bound_cost.cost, CapProd(violation, violation)), + ¤t_cumul_cost_value); } } } if (FilterCumulSoftLowerBounds()) { - current_cumul_cost_value = - CapAdd(current_cumul_cost_value, - GetPathCumulSoftLowerBoundCost(current_path_transits_, r)); + CapAddTo(GetPathCumulSoftLowerBoundCost(current_path_transits_, r), + ¤t_cumul_cost_value); } if (FilterWithDimensionCumulOptimizerForVehicle(vehicle)) { // TODO(user): Return a status from the optimizer to detect failures @@ -1507,7 +1489,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { DCHECK(optimizer != nullptr); const DimensionSchedulingStatus status = optimizer->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, + vehicle, path_accessor_, /*resource=*/nullptr, filter_objective_cost_ ? &lp_cumul_cost_value : nullptr); switch (status) { case DimensionSchedulingStatus::INFEASIBLE: @@ -1516,7 +1498,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { case DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY: DCHECK(mp_optimizer_ != nullptr); if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, + vehicle, path_accessor_, /*resource=*/nullptr, filter_objective_cost_ ? &lp_cumul_cost_value : nullptr) == DimensionSchedulingStatus::INFEASIBLE) { lp_cumul_cost_value = 0; @@ -1534,8 +1516,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { current_max_end_.cumul_value = cumul; current_max_end_.cumul_value_support = r; } - total_current_cumul_cost_value_ = - CapAdd(total_current_cumul_cost_value_, current_cumul_cost_value); + CapAddTo(current_cumul_cost_value, &total_current_cumul_cost_value_); } if (FilterPrecedences()) { // Update the min/max node cumuls of new unperformed nodes. @@ -1546,6 +1527,9 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { // Use the max of the path end cumul mins to compute the corresponding // maximum start cumul of each path; store the minimum of these. for (int r = 0; r < NumPaths(); ++r) { + if (!IsVarSynced(Start(r))) { + continue; + } const int64_t start = ComputePathMaxStartFromEndCumul( current_path_transits_, r, Start(r), current_max_end_.cumul_value); current_min_start_.path_values[r] = start; @@ -1600,10 +1584,10 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/, while (node < Size()) { const int64_t next = GetNext(node); const int64_t transit = (*evaluators_[vehicle])(node, next); - total_transit = CapAdd(total_transit, transit); + CapAddTo(transit, &total_transit); const int64_t transit_slack = CapAdd(transit, slacks_[node]->Min()); delta_path_transits_.PushTransit(path, node, next, transit_slack); - cumul = CapAdd(cumul, transit_slack); + CapAddTo(transit_slack, &cumul); cumul = dimension_.GetFirstPossibleGreaterOrEqualValueForNode(next, cumul); if (cumul > std::min(capacity, cumuls_[next]->Max())) { return false; @@ -1612,10 +1596,8 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/, min_path_cumuls_.push_back(cumul); node = next; if (filter_vehicle_costs) { - cumul_cost_delta = - CapAdd(cumul_cost_delta, GetCumulSoftCost(node, cumul)); - cumul_cost_delta = - CapAdd(cumul_cost_delta, GetCumulPiecewiseLinearCost(node, cumul)); + CapAddTo(GetCumulSoftCost(node, cumul), &cumul_cost_delta); + CapAddTo(GetCumulPiecewiseLinearCost(node, cumul), &cumul_cost_delta); } } const int64_t min_end = cumul; @@ -1663,25 +1645,23 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/, dimension_.GetBreakIntervalsOfVehicle(vehicle)) { if (!br->MustBePerformed()) continue; if (max_start < br->EndMin() && br->StartMax() < min_end) { - min_total_break = CapAdd(min_total_break, br->DurationMin()); + CapAddTo(br->DurationMin(), &min_total_break); } } if (min_total_break > slack_max) return false; min_total_slack = std::max(min_total_slack, min_total_break); } if (filter_vehicle_costs) { - cumul_cost_delta = - CapAdd(cumul_cost_delta, - CapProd(vehicle_total_slack_cost_coefficients_[vehicle], - min_total_slack)); + CapAddTo(CapProd(vehicle_total_slack_cost_coefficients_[vehicle], + min_total_slack), + &cumul_cost_delta); const int64_t span_lower_bound = CapAdd(total_transit, min_total_slack); if (FilterSoftSpanCost()) { const BoundCost bound_cost = dimension_.GetSoftSpanUpperBoundForVehicle(vehicle); if (bound_cost.bound < span_lower_bound) { const int64_t violation = CapSub(span_lower_bound, bound_cost.bound); - cumul_cost_delta = - CapAdd(cumul_cost_delta, CapProd(bound_cost.cost, violation)); + CapAddTo(CapProd(bound_cost.cost, violation), &cumul_cost_delta); } } if (FilterSoftSpanQuadraticCost()) { @@ -1689,9 +1669,8 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/, dimension_.GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle); if (bound_cost.bound < span_lower_bound) { const int64_t violation = CapSub(span_lower_bound, bound_cost.bound); - cumul_cost_delta = - CapAdd(cumul_cost_delta, - CapProd(bound_cost.cost, CapProd(violation, violation))); + CapAddTo(CapProd(bound_cost.cost, CapProd(violation, violation)), + &cumul_cost_delta); } } } @@ -1701,9 +1680,8 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/, } } if (FilterCumulSoftLowerBounds() && filter_vehicle_costs) { - cumul_cost_delta = - CapAdd(cumul_cost_delta, - GetPathCumulSoftLowerBoundCost(delta_path_transits_, path)); + CapAddTo(GetPathCumulSoftLowerBoundCost(delta_path_transits_, path), + &cumul_cost_delta); } if (FilterPrecedences()) { StoreMinMaxCumulOfNodesOnPath(path, min_path_cumuls_, /*is_delta=*/true); @@ -1725,7 +1703,7 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/, delta_max_end_cumul_ = std::max(delta_max_end_cumul_, min_end); } } - cumul_cost_delta_ = CapAdd(cumul_cost_delta_, cumul_cost_delta); + CapAddTo(cumul_cost_delta, &cumul_cost_delta_); return true; } @@ -1851,7 +1829,7 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, int64_t path_delta_cost_with_lp = 0; const DimensionSchedulingStatus status = optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, + vehicle, path_accessor_, /*resource=*/nullptr, filter_objective_cost_ ? &path_delta_cost_with_lp : nullptr); if (status == DimensionSchedulingStatus::INFEASIBLE) { return false; @@ -1861,8 +1839,7 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, path_delta_cost_with_lp, delta_path_cumul_cost_values_[vehicle]); if (path_cost_diff_with_lp > 0) { path_delta_cost_values[i] = path_delta_cost_with_lp; - accepted_objective_value_ = - CapAdd(accepted_objective_value_, path_cost_diff_with_lp); + CapAddTo(path_cost_diff_with_lp, &accepted_objective_value_); if (accepted_objective_value_ > objective_max) { return false; } @@ -1885,7 +1862,7 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, const int vehicle = start_to_vehicle_[start]; int64_t path_delta_cost_with_mp = 0; if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, + vehicle, path_accessor_, /*resource=*/nullptr, filter_objective_cost_ ? &path_delta_cost_with_mp : nullptr) == DimensionSchedulingStatus::INFEASIBLE) { return false; @@ -1894,8 +1871,7 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, const int64_t path_cost_diff_with_mp = CapSub(path_delta_cost_with_mp, path_delta_cost_values[i]); if (path_cost_diff_with_mp > 0) { - accepted_objective_value_ = - CapAdd(accepted_objective_value_, path_cost_diff_with_mp); + CapAddTo(path_cost_diff_with_mp, &accepted_objective_value_); if (accepted_objective_value_ > objective_max) { return false; } @@ -2267,6 +2243,7 @@ namespace { class PickupDeliveryFilter : public BasePathFilter { public: PickupDeliveryFilter(const std::vector& nexts, int next_domain_size, + const PathsMetadata& paths_metadata, const std::vector& pairs, const std::vector& vehicle_policies); @@ -2290,9 +2267,10 @@ class PickupDeliveryFilter : public BasePathFilter { PickupDeliveryFilter::PickupDeliveryFilter( const std::vector& nexts, int next_domain_size, + const PathsMetadata& paths_metadata, const std::vector& pairs, const std::vector& vehicle_policies) - : BasePathFilter(nexts, next_domain_size), + : BasePathFilter(nexts, next_domain_size, paths_metadata), pair_firsts_(next_domain_size, kUnassigned), pair_seconds_(next_domain_size, kUnassigned), pairs_(pairs), @@ -2453,7 +2431,7 @@ IntVarLocalSearchFilter* MakePickupDeliveryFilter( vehicle_policies) { return routing_model.solver()->RevAlloc(new PickupDeliveryFilter( routing_model.Nexts(), routing_model.Size() + routing_model.vehicles(), - pairs, vehicle_policies)); + routing_model.GetPathsMetadata(), pairs, vehicle_policies)); } namespace { @@ -2479,7 +2457,8 @@ class VehicleVarFilter : public BasePathFilter { VehicleVarFilter::VehicleVarFilter(const RoutingModel& routing_model) : BasePathFilter(routing_model.Nexts(), - routing_model.Size() + routing_model.vehicles()), + routing_model.Size() + routing_model.vehicles(), + routing_model.GetPathsMetadata()), vehicle_vars_(routing_model.VehicleVars()), unconstrained_vehicle_var_domain_size_(routing_model.vehicles()), touched_(routing_model.Nexts().size()) { @@ -2696,21 +2675,31 @@ void LPCumulFilter::OnSynchronize(const Assignment* /*delta*/) { : index; }; - const DimensionSchedulingStatus status = - optimizer_.ComputeCumulCostWithoutFixedTransits( - next_accessor, &synchronized_cost_without_transit_); + if (!filter_objective_cost_) { + synchronized_cost_without_transit_ = 0; + } + DimensionSchedulingStatus status = + filter_objective_cost_ + ? optimizer_.ComputeCumulCostWithoutFixedTransits( + next_accessor, &synchronized_cost_without_transit_) + : optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr, + nullptr); if (status == DimensionSchedulingStatus::INFEASIBLE) { // TODO(user): This should only happen if the LP solver times out. // DCHECK the fail wasn't due to an infeasible model. synchronized_cost_without_transit_ = 0; } - if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY && - mp_optimizer_.ComputeCumulCostWithoutFixedTransits( - next_accessor, &synchronized_cost_without_transit_) != - DimensionSchedulingStatus::OPTIMAL) { + if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { + status = filter_objective_cost_ + ? mp_optimizer_.ComputeCumulCostWithoutFixedTransits( + next_accessor, &synchronized_cost_without_transit_) + : mp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, + nullptr, nullptr); + if (status != DimensionSchedulingStatus::OPTIMAL) { // TODO(user): This should only happen if the MP solver times out. // DCHECK the fail wasn't due to an infeasible model. synchronized_cost_without_transit_ = 0; + } } } @@ -2742,8 +2731,7 @@ class ResourceGroupAssignmentFilter : public BasePathFilter { LocalDimensionCumulOptimizer* mp_optimizer, bool filter_objective_cost); bool InitializeAcceptPath() override; - bool AcceptPath(int64_t path_start, int64_t chain_start, - int64_t chain_end) override; + bool AcceptPath(int64_t path_start, int64_t, int64_t) override; bool FinalizeAcceptPath(int64_t objective_min, int64_t objective_max) override; void OnBeforeSynchronizePaths() override; @@ -2761,6 +2749,14 @@ class ResourceGroupAssignmentFilter : public BasePathFilter { } private: + using RCIndex = RoutingModel::ResourceClassIndex; + + bool VehicleRequiresResourceAssignment( + int vehicle, const std::function& next_accessor, + bool* is_infeasible); + int64_t ComputeRouteCumulCostWithoutResourceAssignment( + int vehicle, const std::function& next_accessor) const; + const RoutingModel& model_; const RoutingDimension& dimension_; const ResourceGroup& resource_group_; @@ -2771,15 +2767,24 @@ class ResourceGroupAssignmentFilter : public BasePathFilter { int64_t synchronized_cost_without_transit_; int64_t delta_cost_without_transit_; std::vector> vehicle_to_resource_class_assignment_costs_; + std::vector vehicles_requiring_resource_assignment_; + std::vector vehicle_requires_resource_assignment_; std::vector> delta_vehicle_to_resource_class_assignment_costs_; + std::vector delta_vehicles_requiring_resource_assignment_; + std::vector delta_vehicle_requires_resource_assignment_; + + std::vector bound_resource_index_of_vehicle_; + absl::StrongVector> + ignored_resources_per_class_; }; ResourceGroupAssignmentFilter::ResourceGroupAssignmentFilter( const std::vector& nexts, const ResourceGroup* resource_group, LocalDimensionCumulOptimizer* lp_optimizer, LocalDimensionCumulOptimizer* mp_optimizer, bool filter_objective_cost) - : BasePathFilter(nexts, lp_optimizer->dimension()->cumuls().size()), + : BasePathFilter(nexts, lp_optimizer->dimension()->cumuls().size(), + lp_optimizer->dimension()->model()->GetPathsMetadata()), model_(*lp_optimizer->dimension()->model()), dimension_(*lp_optimizer->dimension()), resource_group_(*resource_group), @@ -2796,6 +2801,9 @@ ResourceGroupAssignmentFilter::ResourceGroupAssignmentFilter( bool ResourceGroupAssignmentFilter::InitializeAcceptPath() { delta_vehicle_to_resource_class_assignment_costs_.assign(model_.vehicles(), {}); + if (current_synch_failed_) { + return true; + } // TODO(user): Keep track of num_used_vehicles internally and compute its // new value here by only going through the touched_paths_. int num_used_vehicles = 0; @@ -2808,37 +2816,77 @@ bool ResourceGroupAssignmentFilter::InitializeAcceptPath() { } } } + delta_vehicle_requires_resource_assignment_ = + vehicle_requires_resource_assignment_; return true; } -bool ResourceGroupAssignmentFilter::AcceptPath(int64_t path_start, - int64_t /*chain_start*/, - int64_t /*chain_end*/) { +bool ResourceGroupAssignmentFilter::AcceptPath(int64_t path_start, int64_t, + int64_t) { + if (current_synch_failed_) { + return true; + } const int vehicle = model_.VehicleIndex(path_start); - // TODO(user): Make delta_vehicles_requiring_resource_assignment_ and - // delta_ignored_resources_per_class_ class members, properly update them in - // AcceptPath(), and delay calls to - // ComputeVehicleToResourceClassAssignmentCosts() to FinalizeAcceptPath(). - using RCIndex = RoutingModel::ResourceClassIndex; - const absl::StrongVector> - ignored_resources_per_class(resource_group_.GetResourceClassesCount()); - return ComputeVehicleToResourceClassAssignmentCosts( - vehicle, resource_group_, ignored_resources_per_class, - [this](int64_t index) { return GetNext(index); }, - dimension_.transit_evaluator(vehicle), filter_objective_cost_, - lp_optimizer_, mp_optimizer_, - &delta_vehicle_to_resource_class_assignment_costs_[vehicle], nullptr, - nullptr); + bool is_infeasible = false; + delta_vehicle_requires_resource_assignment_[vehicle] = + VehicleRequiresResourceAssignment( + vehicle, [this](int64_t n) { return GetNext(n); }, &is_infeasible); + return !is_infeasible; } bool ResourceGroupAssignmentFilter::FinalizeAcceptPath( int64_t /*objective_min*/, int64_t objective_max) { - using RCIndex = RoutingModel::ResourceClassIndex; - const absl::StrongVector> - ignored_resources_per_class(resource_group_.GetResourceClassesCount()); - delta_cost_without_transit_ = ComputeBestVehicleToResourceAssignment( - resource_group_.GetVehiclesRequiringAResource(), - resource_group_.GetResourceIndicesPerClass(), ignored_resources_per_class, + delta_cost_without_transit_ = 0; + if (current_synch_failed_) { + return true; + } + const auto& next_accessor = [this](int64_t index) { return GetNext(index); }; + delta_vehicles_requiring_resource_assignment_.clear(); + // First sum the costs of the routes not requiring resource assignment + // (cheaper computations). + for (int v = 0; v < model_.vehicles(); ++v) { + if (delta_vehicle_requires_resource_assignment_[v]) { + delta_vehicles_requiring_resource_assignment_.push_back(v); + continue; + } + int64_t route_cost = 0; + int64_t start = model_.Start(v); + if (PathStartTouched(start)) { + route_cost = + ComputeRouteCumulCostWithoutResourceAssignment(v, next_accessor); + if (route_cost < 0) { + return false; + } + } else if (IsVarSynced(start)) { + DCHECK_EQ(vehicle_to_resource_class_assignment_costs_[v].size(), 1); + route_cost = vehicle_to_resource_class_assignment_costs_[v][0]; + } + CapAddTo(route_cost, &delta_cost_without_transit_); + if (delta_cost_without_transit_ > objective_max) { + return false; + } + } + // Recompute the assignment costs to resources for touched paths requiring + // resource assignment. + for (int64_t start : GetTouchedPathStarts()) { + const int vehicle = model_.VehicleIndex(start); + if (!delta_vehicle_requires_resource_assignment_[vehicle]) { + // Already handled above. + continue; + } + if (!ComputeVehicleToResourceClassAssignmentCosts( + vehicle, resource_group_, ignored_resources_per_class_, + next_accessor, dimension_.transit_evaluator(vehicle), + filter_objective_cost_, lp_optimizer_, mp_optimizer_, + &delta_vehicle_to_resource_class_assignment_costs_[vehicle], + nullptr, nullptr)) { + return false; + } + } + const int64_t assignment_cost = ComputeBestVehicleToResourceAssignment( + delta_vehicles_requiring_resource_assignment_, + resource_group_.GetResourceIndicesPerClass(), + ignored_resources_per_class_, /*vehicle_to_resource_class_assignment_costs=*/ [this](int v) { return PathStartTouched(model_.Start(v)) @@ -2846,35 +2894,63 @@ bool ResourceGroupAssignmentFilter::FinalizeAcceptPath( : &vehicle_to_resource_class_assignment_costs_[v]; }, nullptr); - return delta_cost_without_transit_ >= 0 && - delta_cost_without_transit_ <= objective_max; + CapAddTo(assignment_cost, &delta_cost_without_transit_); + return assignment_cost >= 0 && delta_cost_without_transit_ <= objective_max; } void ResourceGroupAssignmentFilter::OnBeforeSynchronizePaths() { - current_synch_failed_ = false; + if (!HasAnySyncedPath()) { + vehicle_to_resource_class_assignment_costs_.assign(model_.vehicles(), {}); + } + bound_resource_index_of_vehicle_.assign(model_.vehicles(), -1); + vehicles_requiring_resource_assignment_.clear(); + vehicles_requiring_resource_assignment_.reserve( + resource_group_.GetVehiclesRequiringAResource().size()); + vehicle_requires_resource_assignment_.assign(model_.vehicles(), false); + ignored_resources_per_class_.assign(resource_group_.GetResourceClassesCount(), + absl::flat_hash_set()); + + for (int v : resource_group_.GetVehiclesRequiringAResource()) { + const int64_t start = model_.Start(v); + if (!IsVarSynced(start)) { + continue; + } + vehicle_requires_resource_assignment_[v] = + VehicleRequiresResourceAssignment( + v, [this](int64_t n) { return Value(n); }, ¤t_synch_failed_); + if (vehicle_requires_resource_assignment_[v]) { + vehicles_requiring_resource_assignment_.push_back(v); + } + if (current_synch_failed_) { + return; + } + } + synchronized_cost_without_transit_ = 0; } void ResourceGroupAssignmentFilter::OnSynchronizePathFromStart(int64_t start) { - // NOTE(user): Even if filter_objective_cost_ is false, we still need to - // call ComputeVehicleToResourceClassAssignmentCosts() for every vehicle to - // keep track of whether or not a given vehicle-to-resource-class assignment - // is possible by storing 0 or -1 in - // vehicle_to_resource_class_assignment_costs_. - const auto& next_accessor = [this](int64_t index) { - return IsVarSynced(index) ? Value(index) - : model_.IsStart(index) ? model_.End(model_.VehicleIndex(index)) - : index; - }; + if (current_synch_failed_) return; + DCHECK(IsVarSynced(start)); const int v = model_.VehicleIndex(start); - // TODO(user): Make vehicles_requiring_resource_assignment_ and - // ignored_resources_per_class_ class members, properly update them in - // OnSynchronizePathFromStart(), and delay calls to - // ComputeVehicleToResourceClassAssignmentCosts() to OnAfterSynchronizePaths() - using RCIndex = RoutingModel::ResourceClassIndex; - const absl::StrongVector> - ignored_resources_per_class(resource_group_.GetResourceClassesCount()); + const auto& next_accessor = [this](int64_t index) { return Value(index); }; + if (!vehicle_requires_resource_assignment_[v]) { + const int64_t route_cost = + ComputeRouteCumulCostWithoutResourceAssignment(v, next_accessor); + if (route_cost < 0) { + current_synch_failed_ = true; + return; + } + CapAddTo(route_cost, &synchronized_cost_without_transit_); + vehicle_to_resource_class_assignment_costs_[v] = {route_cost}; + return; + } + // NOTE(user): Even if filter_objective_cost_ is false, we still need to + // call ComputeVehicleToResourceClassAssignmentCosts() for every vehicle + // requiring resource assignment to keep track of whether or not a given + // vehicle-to-resource-class assignment is possible by storing 0 or -1 in + // vehicle_to_resource_class_assignment_costs_. if (!ComputeVehicleToResourceClassAssignmentCosts( - v, resource_group_, ignored_resources_per_class, next_accessor, + v, resource_group_, ignored_resources_per_class_, next_accessor, dimension_.transit_evaluator(v), filter_objective_cost_, lp_optimizer_, mp_optimizer_, &vehicle_to_resource_class_assignment_costs_[v], nullptr, nullptr)) { @@ -2885,22 +2961,95 @@ void ResourceGroupAssignmentFilter::OnSynchronizePathFromStart(int64_t start) { } void ResourceGroupAssignmentFilter::OnAfterSynchronizePaths() { - using RCIndex = RoutingModel::ResourceClassIndex; - const absl::StrongVector> - ignored_resources_per_class(resource_group_.GetResourceClassesCount()); - synchronized_cost_without_transit_ = - (current_synch_failed_ || !filter_objective_cost_) - ? 0 - : ComputeBestVehicleToResourceAssignment( - resource_group_.GetVehiclesRequiringAResource(), + if (current_synch_failed_) { + synchronized_cost_without_transit_ = 0; + return; + } + if (!filter_objective_cost_) { + DCHECK_EQ(synchronized_cost_without_transit_, 0); + return; + } + const int64_t assignment_cost = ComputeBestVehicleToResourceAssignment( + vehicles_requiring_resource_assignment_, resource_group_.GetResourceIndicesPerClass(), - ignored_resources_per_class, - [this](int v) { - return &vehicle_to_resource_class_assignment_costs_[v]; - }, + ignored_resources_per_class_, + [this](int v) { return &vehicle_to_resource_class_assignment_costs_[v]; }, nullptr); - synchronized_cost_without_transit_ = - std::max(synchronized_cost_without_transit_, 0); + if (assignment_cost < 0) { + synchronized_cost_without_transit_ = 0; + current_synch_failed_ = true; + } else { + DCHECK_GE(synchronized_cost_without_transit_, 0); + CapAddTo(assignment_cost, &synchronized_cost_without_transit_); + } +} + +bool ResourceGroupAssignmentFilter::VehicleRequiresResourceAssignment( + int vehicle, const std::function& next_accessor, + bool* is_infeasible) { + *is_infeasible = false; + if (!resource_group_.VehicleRequiresAResource(vehicle)) return false; + const IntVar* res_var = model_.ResourceVar(vehicle, resource_group_.Index()); + if (!model_.IsVehicleUsedWhenEmpty(vehicle) && + next_accessor(model_.Start(vehicle)) == model_.End(vehicle)) { + if (res_var->Bound() && res_var->Value() >= 0) { + // Vehicle with a resource (force-)assigned to it cannot be unused. + *is_infeasible = true; + } + return false; + } + // Vehicle is used. + if (res_var->Bound()) { + // No need to do resource assignment for this vehicle. + const int res = res_var->Value(); + if (res < 0) { + // Vehicle has a negative resource index enforced but is used. + *is_infeasible = true; + } else { + bound_resource_index_of_vehicle_[vehicle] = res; + const RCIndex rc = resource_group_.GetResourceClassIndex(res); + ignored_resources_per_class_[rc].insert(res); + } + return false; + } + // Vehicle is used and res_var isn't bound. + return true; +} + +int64_t +ResourceGroupAssignmentFilter::ComputeRouteCumulCostWithoutResourceAssignment( + int vehicle, const std::function& next_accessor) const { + if (next_accessor(model_.Start(vehicle)) == model_.End(vehicle) && + !model_.IsVehicleUsedWhenEmpty(vehicle)) { + return 0; + } + using Resource = RoutingModel::ResourceGroup::Resource; + const Resource* resource = nullptr; + if (resource_group_.VehicleRequiresAResource(vehicle)) { + DCHECK_GE(bound_resource_index_of_vehicle_[vehicle], 0); + resource = + &resource_group_.GetResource(bound_resource_index_of_vehicle_[vehicle]); + } + int64_t route_cost = 0; + const DimensionSchedulingStatus status = + lp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( + vehicle, next_accessor, resource, + filter_objective_cost_ ? &route_cost : nullptr); + switch (status) { + case DimensionSchedulingStatus::INFEASIBLE: + return -1; + case DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY: + if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( + vehicle, next_accessor, resource, + filter_objective_cost_ ? &route_cost : nullptr) == + DimensionSchedulingStatus::INFEASIBLE) { + return -1; + } + break; + default: + DCHECK(status == DimensionSchedulingStatus::OPTIMAL); + } + return route_cost; } // ResourceAssignmentFilter @@ -3093,27 +3242,29 @@ IntVarLocalSearchFilter* MakeCPFeasibilityFilter(RoutingModel* routing_model) { } PathEnergyCostChecker::PathEnergyCostChecker( - const PathState* path_state, std::vector force_class, + const PathState* path_state, std::vector force_start_min, + std::vector force_end_min, std::vector force_class, std::vector*> force_per_class, std::vector distance_class, std::vector*> distance_per_class, - std::vector path_unit_costs, + std::vector path_energy_cost, std::vector path_has_cost_when_empty) : path_state_(path_state), + force_start_min_(std::move(force_start_min)), + force_end_min_(std::move(force_end_min)), force_class_(std::move(force_class)), distance_class_(std::move(distance_class)), force_per_class_(std::move(force_per_class)), distance_per_class_(std::move(distance_per_class)), - path_unit_costs_(std::move(path_unit_costs)), + path_energy_cost_(std::move(path_energy_cost)), path_has_cost_when_empty_(std::move(path_has_cost_when_empty)) { committed_total_cost_ = 0; committed_path_cost_.assign(path_state_->NumPaths(), 0); const int num_paths = path_state_->NumPaths(); for (int path = 0; path < num_paths; ++path) { committed_path_cost_[path] = ComputePathCost(path); - committed_total_cost_ = - CapAdd(committed_total_cost_, committed_path_cost_[path]); + CapAddTo(committed_path_cost_[path], &committed_total_cost_); } accepted_total_cost_ = committed_total_cost_; } @@ -3124,7 +3275,7 @@ bool PathEnergyCostChecker::Check() { for (const int path : path_state_->ChangedPaths()) { accepted_total_cost_ = CapSub(accepted_total_cost_, committed_path_cost_[path]); - accepted_total_cost_ = CapAdd(accepted_total_cost_, ComputePathCost(path)); + CapAddTo(ComputePathCost(path), &accepted_total_cost_); if (accepted_total_cost_ == kint64max) return false; } return true; @@ -3135,8 +3286,7 @@ void PathEnergyCostChecker::Commit() { committed_total_cost_ = CapSub(committed_total_cost_, committed_path_cost_[path]); committed_path_cost_[path] = ComputePathCost(path); - committed_total_cost_ = - CapAdd(committed_total_cost_, committed_path_cost_[path]); + CapAddTo(committed_path_cost_[path], &committed_total_cost_); } } @@ -3146,36 +3296,54 @@ int64_t PathEnergyCostChecker::ComputePathCost(int64_t path) const { const int distance_class = distance_class_[path]; const auto& distance_evaluator = *distance_per_class_[distance_class]; - int64_t total_energy = 0; - int64_t total_force = 0; - int64_t min_total_force = kint64max; - int64_t total_distance = 0; + // Find minimal force at which to start. + int64_t force = force_start_min_[path]; + int64_t min_force = force; int num_path_nodes = 0; int prev_node = path_state_->Start(path); for (const auto chain : path_state_->Chains(path)) { num_path_nodes += chain.NumNodes(); - const int first = chain.First(); - // Add energy needed to go from prev_node to chain.First() if needed, - // then add energy needed to go from chain.First() to chain.Last(). + // Add force needed to go from prev_node to chain.First() if needed, + // then add force needed to go from chain.First() to chain.Last(). for (const int node : - (first == prev_node ? chain.WithoutFirstNode() : chain)) { - const int64_t force = force_evaluator(prev_node); - const int64_t distance = distance_evaluator(prev_node, node); - total_force = CapAdd(total_force, force); - total_energy = CapAdd(total_energy, CapProd(total_force, distance)); - total_distance = CapAdd(total_distance, distance); - min_total_force = std::min(min_total_force, total_force); + (chain.First() == prev_node ? chain.WithoutFirstNode() : chain)) { + const int64_t force_to_node = force_evaluator(prev_node); + CapAddTo(force_to_node, &force); + min_force = std::min(min_force, force); prev_node = node; } } - // If total_force was ever < 0, offset it to 0. - if (min_total_force < 0) { - const int64_t offset = CapProd(total_distance, CapOpp(min_total_force)); - total_energy = CapAdd(total_energy, offset); + if (num_path_nodes == 2 && !path_has_cost_when_empty_[path]) return 0; + // Force must be offset in order to be all of: + // - >= force_start_min_[path] at start + // - >= force_end_min_[path] at end + // - >= 0 at all intermediate nodes + // We set the accumulator to the minimal offset that allows this. + force = std::max( + {0, CapOpp(min_force), CapSub(force_end_min_[path], force)}); + CapAddTo(force_start_min_[path], &force); + + // Compute energy, below and above threshold. + const EnergyCost& cost = path_energy_cost_[path]; + int64_t energy_below = 0; + int64_t energy_above = 0; + prev_node = path_state_->Start(path); + for (const auto chain : path_state_->Chains(path)) { + for (const int node : + (chain.First() == prev_node ? chain.WithoutFirstNode() : chain)) { + const int64_t distance = distance_evaluator(prev_node, node); + CapAddTo(force_evaluator(prev_node), &force); + CapAddTo(CapProd(std::min(cost.threshold, force), distance), + &energy_below); + const int64_t force_above = + std::max(0, CapSub(force, cost.threshold)); + CapAddTo(CapProd(force_above, distance), &energy_above); + prev_node = node; } - return (num_path_nodes == 2 && !path_has_cost_when_empty_[path]) - ? 0 - : CapProd(total_energy, path_unit_costs_[path]); + } + + return CapAdd(CapProd(energy_below, cost.cost_per_unit_below_threshold), + CapProd(energy_above, cost.cost_per_unit_above_threshold)); } namespace { diff --git a/ortools/constraint_solver/routing_filters.h b/ortools/constraint_solver/routing_filters.h index 5a22e84121..4e372758ff 100644 --- a/ortools/constraint_solver/routing_filters.h +++ b/ortools/constraint_solver/routing_filters.h @@ -86,13 +86,23 @@ IntVarLocalSearchFilter* MakeCPFeasibilityFilter(RoutingModel* routing_model); class PathEnergyCostChecker { public: + struct EnergyCost { + int64_t threshold; + int64_t cost_per_unit_below_threshold; + int64_t cost_per_unit_above_threshold; + bool IsNull() const { + return (cost_per_unit_below_threshold == 0 || threshold == 0) && + (cost_per_unit_above_threshold == 0 || threshold == kint64max); + } + }; PathEnergyCostChecker( - const PathState* path_state, std::vector force_class, + const PathState* path_state, std::vector force_start_min, + std::vector force_end_min, std::vector force_class, std::vector*> force_per_class, std::vector distance_class, std::vector*> distance_per_class, - std::vector path_unit_costs, + std::vector path_energy_cost, std::vector path_has_cost_when_empty); bool Check(); void Commit(); @@ -100,14 +110,16 @@ class PathEnergyCostChecker { int64_t AcceptedCost() const { return accepted_total_cost_; } private: - int64_t ComputePathCost(int64_t path_start) const; + int64_t ComputePathCost(int64_t path) const; const PathState* const path_state_; + const std::vector force_start_min_; + const std::vector force_end_min_; const std::vector force_class_; const std::vector distance_class_; const std::vector*> force_per_class_; const std::vector*> distance_per_class_; - const std::vector path_unit_costs_; + const std::vector path_energy_cost_; const std::vector path_has_cost_when_empty_; // Incremental cost computation. @@ -137,7 +149,8 @@ void AppendDimensionCumulFilters( class BasePathFilter : public IntVarLocalSearchFilter { public: - BasePathFilter(const std::vector& nexts, int next_domain_size); + BasePathFilter(const std::vector& nexts, int next_domain_size, + const PathsMetadata& paths_metadata); ~BasePathFilter() override {} bool Accept(const Assignment* delta, const Assignment* deltadelta, int64_t objective_min, int64_t objective_max) override; @@ -151,9 +164,16 @@ class BasePathFilter : public IntVarLocalSearchFilter { ? (IsVarSynced(node) ? Value(node) : kUnassigned) : new_nexts_[node]; } - int NumPaths() const { return starts_.size(); } - int64_t Start(int i) const { return starts_[i]; } - int GetPath(int64_t node) const { return paths_[node]; } + bool HasAnySyncedPath() const { + for (int64_t start : paths_metadata_.Starts()) { + if (IsVarSynced(start)) return true; + } + return false; + } + int NumPaths() const { return paths_metadata_.NumPaths(); } + int64_t Start(int i) const { return paths_metadata_.Start(i); } + int64_t End(int i) const { return paths_metadata_.End(i); } + int GetPath(int64_t node) const { return paths_metadata_.GetPath(node); } int Rank(int64_t node) const { return ranks_[node]; } bool IsDisabled() const { return status_ == DISABLED; } const std::vector& GetTouchedPathStarts() const { @@ -185,9 +205,8 @@ class BasePathFilter : public IntVarLocalSearchFilter { void UpdateAllRanks(); void UpdatePathRanksFromStart(int start); + const PathsMetadata& paths_metadata_; std::vector node_path_starts_; - std::vector starts_; - std::vector paths_; SparseBitset new_synchronized_unperformed_nodes_; std::vector new_nexts_; std::vector delta_touched_; diff --git a/ortools/constraint_solver/routing_flow.cc b/ortools/constraint_solver/routing_flow.cc index 90e2543c97..67e68f6232 100644 --- a/ortools/constraint_solver/routing_flow.cc +++ b/ortools/constraint_solver/routing_flow.cc @@ -291,7 +291,7 @@ bool RoutingModel::SolveMatchingModel( [&nexts](int64_t node) { return nexts.find(node)->second; }, - &cumul_cost_value) != + /*resource=*/nullptr, &cumul_cost_value) != DimensionSchedulingStatus::INFEASIBLE) { cost = CapAdd(cost, cumul_cost_value); } else { @@ -316,7 +316,7 @@ bool RoutingModel::SolveMatchingModel( [&nexts](int64_t node) { return nexts.find(node)->second; }, - &cumul_cost_value) != + /*resource=*/nullptr, &cumul_cost_value) != DimensionSchedulingStatus::INFEASIBLE) { cost = CapAdd(cost, cumul_cost_value); } else { diff --git a/ortools/constraint_solver/routing_ils.cc b/ortools/constraint_solver/routing_ils.cc index ff96ef8b28..656571d645 100644 --- a/ortools/constraint_solver/routing_ils.cc +++ b/ortools/constraint_solver/routing_ils.cc @@ -13,16 +13,21 @@ #include "ortools/constraint_solver/routing_ils.h" +#include +#include #include #include #include #include +#include #include #include #include "absl/functional/bind_front.h" #include "absl/log/check.h" +#include "absl/time/time.h" #include "ortools/base/logging.h" +#include "ortools/base/protoutil.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/routing.h" #include "ortools/constraint_solver/routing_ils.pb.h" @@ -53,14 +58,24 @@ MakeGlobalCheapestInsertionParameters( return gci_parameters; } +SavingsFilteredHeuristic::SavingsParameters MakeSavingsParameters( + const RoutingSearchParameters& search_parameters) { + SavingsFilteredHeuristic::SavingsParameters savings_parameters; + savings_parameters.neighbors_ratio = + search_parameters.savings_neighbors_ratio(); + savings_parameters.max_memory_usage_bytes = + search_parameters.savings_max_memory_usage_bytes(); + savings_parameters.add_reverse_arcs = + search_parameters.savings_add_reverse_arcs(); + savings_parameters.arc_coefficient = + search_parameters.savings_arc_coefficient(); + return savings_parameters; +} + // Returns a ruin procedure based to the given parameters. std::unique_ptr MakeRuinProcedure( const RuinRecreateParameters& parameters, RoutingModel* model) { switch (parameters.ruin_strategy()) { - case RuinStrategy::UNSET: - LOG(ERROR) << "Unset ruin procedure, using " - "RuinStrategy::SPATIALLY_CLOSE_ROUTES_REMOVAL."; - [[fallthrough]]; case RuinStrategy::SPATIALLY_CLOSE_ROUTES_REMOVAL: return std::make_unique( model, parameters.num_ruined_routes()); @@ -79,10 +94,6 @@ std::unique_ptr MakeRecreateProcedure( switch (parameters.iterated_local_search_parameters() .ruin_recreate_parameters() .recreate_strategy()) { - case FirstSolutionStrategy::UNSET: - LOG(ERROR) << "Unset recreate procedure, using " - "FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION"; - [[fallthrough]]; case FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION: return std::make_unique( model, std::move(stop_search), @@ -117,6 +128,16 @@ std::unique_ptr MakeRecreateProcedure( [model](int64_t i) { return model->UnperformedPenaltyOrValue(0, i); }, filter_manager, gci_parameters); } + case FirstSolutionStrategy::SAVINGS: { + return std::make_unique( + model, std::move(stop_search), MakeSavingsParameters(parameters), + filter_manager); + } + case FirstSolutionStrategy::PARALLEL_SAVINGS: { + return std::make_unique( + model, std::move(stop_search), MakeSavingsParameters(parameters), + filter_manager); + } default: LOG(ERROR) << "Unsupported recreate procedure."; return nullptr; @@ -129,12 +150,109 @@ std::unique_ptr MakeRecreateProcedure( // improving candidate assignment. class GreedyDescentAcceptanceCriterion : public NeighborAcceptanceCriterion { public: - bool Accept(const Assignment* candidate, - const Assignment* reference) const override { + bool Accept([[maybe_unused]] const SearchState& search_state, + const Assignment* candidate, + const Assignment* reference) override { return candidate->ObjectiveValue() < reference->ObjectiveValue(); } }; +// Simulated annealing cooling schedule interface. +class CoolingSchedule { + public: + CoolingSchedule(NeighborAcceptanceCriterion::SearchState final_search_state, + double initial_temperature) + : final_search_state_(std::move(final_search_state)), + initial_temperature_(initial_temperature) {} + virtual ~CoolingSchedule() = default; + + // Returns the temperature according to given search state. + virtual double GetTemperature( + const NeighborAcceptanceCriterion::SearchState& search_state) const = 0; + + protected: + const NeighborAcceptanceCriterion::SearchState final_search_state_; + const double initial_temperature_; +}; + +// A cooling schedule that lowers the temperature in an exponential way. +class ExponentialCoolingSchedule : public CoolingSchedule { + public: + ExponentialCoolingSchedule( + NeighborAcceptanceCriterion::SearchState final_search_state, + double initial_temperature, double final_temperature) + : CoolingSchedule(std::move(final_search_state), initial_temperature), + temperature_ratio_(final_temperature / initial_temperature) {} + + double GetTemperature(const NeighborAcceptanceCriterion::SearchState& + search_state) const override { + const double duration_progress = + absl::FDivDuration(search_state.duration, final_search_state_.duration); + const double solutions_progress = + static_cast(search_state.solutions) / + final_search_state_.solutions; + // We take the min with 1 as at the end of the search we may go a bit above + // 1 with duration_progress depending on when we check the time limit. + const double progress = + std::min(1.0, std::max(duration_progress, solutions_progress)); + return initial_temperature_ * std::pow(temperature_ratio_, progress); + } + + private: + const double temperature_ratio_; +}; + +std::unique_ptr MakeCoolingSchedule( + const RoutingSearchParameters& parameters) { + const absl::Duration final_duration = + !parameters.has_time_limit() + ? absl::InfiniteDuration() + : util_time::DecodeGoogleApiProto(parameters.time_limit()).value(); + + const SimulatedAnnealingParameters& sa_params = + parameters.iterated_local_search_parameters() + .simulated_annealing_parameters(); + + switch (sa_params.cooling_schedule_strategy()) { + case CoolingScheduleStrategy::EXPONENTIAL: + return std::make_unique( + NeighborAcceptanceCriterion::SearchState{final_duration, + parameters.solution_limit()}, + sa_params.initial_temperature(), sa_params.final_temperature()); + default: + LOG(ERROR) << "Unsupported cooling schedule strategy."; + return nullptr; + } +} + +// Simulated annealing acceptance criterion in which the reference assignment is +// replaced with a probability given by the quality of the candidate solution, +// the current search state and the chosen cooling schedule. +class SimulatedAnnealingAcceptanceCriterion + : public NeighborAcceptanceCriterion { + public: + explicit SimulatedAnnealingAcceptanceCriterion( + std::unique_ptr cooling_schedule) + : cooling_schedule_(std::move(cooling_schedule)), + rnd_(std::mt19937(/*seed=*/0)), + probability_distribution_(0.0, 1.0) {} + + bool Accept(const SearchState& search_state, const Assignment* candidate, + const Assignment* reference) override { + double temperature = cooling_schedule_->GetTemperature(search_state); + return candidate->ObjectiveValue() + + temperature * std::log(probability_distribution_(rnd_)) < + reference->ObjectiveValue(); + } + + private: + std::unique_ptr cooling_schedule_; + // TODO(user): consider sharing a single rnd engine among all ILS + // components. + std::mt19937 rnd_; + std::uniform_real_distribution probability_distribution_; +}; + } // namespace CloseRoutesRemovalRuinProcedure::CloseRoutesRemovalRuinProcedure( @@ -253,10 +371,6 @@ DecisionBuilder* MakePerturbationDecisionBuilder( LocalSearchFilterManager* filter_manager) { switch ( parameters.iterated_local_search_parameters().perturbation_strategy()) { - case PerturbationStrategy::UNSET: - LOG(ERROR) << "Unset perturbation strategy, using " - "PerturbationStrategy::RUIN_AND_RECREATE."; - [[fallthrough]]; case PerturbationStrategy::RUIN_AND_RECREATE: return MakeRuinAndRecreateDecisionBuilder(parameters, model, assignment, std::move(stop_search), @@ -271,12 +385,11 @@ std::unique_ptr MakeNeighborAcceptanceCriterion( const RoutingSearchParameters& parameters) { CHECK(parameters.has_iterated_local_search_parameters()); switch (parameters.iterated_local_search_parameters().acceptance_strategy()) { - case AcceptanceStrategy::UNSET: - LOG(ERROR) << "Unset acceptance strategy, using " - "AcceptanceStrategy::GREEDY_DESCENT."; - [[fallthrough]]; case AcceptanceStrategy::GREEDY_DESCENT: return std::make_unique(); + case AcceptanceStrategy::SIMULATED_ANNEALING: + return std::make_unique( + MakeCoolingSchedule(parameters)); default: LOG(ERROR) << "Unsupported acceptance strategy."; return nullptr; diff --git a/ortools/constraint_solver/routing_ils.h b/ortools/constraint_solver/routing_ils.h index fd0f56e970..b824ba12bf 100644 --- a/ortools/constraint_solver/routing_ils.h +++ b/ortools/constraint_solver/routing_ils.h @@ -20,6 +20,7 @@ #include #include +#include "absl/time/time.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/routing.h" #include "ortools/constraint_solver/routing_parameters.pb.h" @@ -67,10 +68,20 @@ DecisionBuilder* MakePerturbationDecisionBuilder( // Neighbor acceptance criterion interface. class NeighborAcceptanceCriterion { public: + // Representation of the search process state. + struct SearchState { + // Search duration. + absl::Duration duration; + // Explored solutions. + int64_t solutions; + }; + virtual ~NeighborAcceptanceCriterion() = default; - // Returns whether `candidate` should replace `reference`. - virtual bool Accept(const Assignment* candidate, - const Assignment* reference) const = 0; + // Returns whether `candidate` should replace `reference` given the provided + // search state. + virtual bool Accept(const SearchState& search_state, + const Assignment* candidate, + const Assignment* reference) = 0; }; // Returns a neighbor acceptance criterion based on the given parameters. diff --git a/ortools/constraint_solver/routing_ils.proto b/ortools/constraint_solver/routing_ils.proto index 562d0c9fda..0f3076de3b 100644 --- a/ortools/constraint_solver/routing_ils.proto +++ b/ortools/constraint_solver/routing_ils.proto @@ -45,7 +45,7 @@ message RuinRecreateParameters { FirstSolutionStrategy.Value recreate_strategy = 2; // Number of routes removed during a ruin application defined on routes. - uint32 num_ruined_routes = 3; + optional uint32 num_ruined_routes = 3; } // Defines how a reference solution is perturbed. @@ -59,6 +59,34 @@ message PerturbationStrategy { } } +// The cooling schedule strategy defines how the simulated annealing temperature +// moves from the initial to the final value. +message CoolingScheduleStrategy { + enum Value { + // Unspecified value. + UNSET = 0; + + // Exponential cooldown. + EXPONENTIAL = 1; + } +} + +// Specifies the behavior of a simulated annealing acceptance strategy. +message SimulatedAnnealingParameters { + // Determines the speed at which the temperature changes from initial to + // final. + CoolingScheduleStrategy.Value cooling_schedule_strategy = 1; + + // The initial temperature. + optional double initial_temperature = 2; + + // The final temperature. + optional double final_temperature = 3; + + // TODO(user): support automatic definition of initial and final + // temperatures. +} + // Determines when a neighbor solution, obtained by the application of a // perturbation and improvement step to a reference solution, is used to // replace the reference solution. @@ -67,9 +95,13 @@ message AcceptanceStrategy { // Unspecified value. UNSET = 0; - // Accept only solutions that are improving with respect to the reference + // Accepts only solutions that are improving with respect to the reference // one. GREEDY_DESCENT = 1; + + // Accepts a candidate solution with a probability that depends on its + // quality and on the current state of the search. + SIMULATED_ANNEALING = 2; } } @@ -84,9 +116,13 @@ message IteratedLocalSearchParameters { // Determines whether solution S', obtained from the perturbation, should be // optimized with a local search application. - bool improve_perturbed_solution = 3; + optional bool improve_perturbed_solution = 3; // Determines when the neighbor solution S', possibly improved if // `improve_perturbed_solution` is true, replaces the reference solution S. AcceptanceStrategy.Value acceptance_strategy = 4; + + // Parameters to customize a simulated annealing acceptance strategy. These + // parameters are required iff the acceptance_strategy is SIMULATED_ANNEALING. + SimulatedAnnealingParameters simulated_annealing_parameters = 5; } diff --git a/ortools/constraint_solver/routing_lp_scheduling.cc b/ortools/constraint_solver/routing_lp_scheduling.cc index 5df8dc4c17..c2536293ab 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.cc +++ b/ortools/constraint_solver/routing_lp_scheduling.cc @@ -35,6 +35,7 @@ #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/span.h" +#include "ortools/base/dump_vars.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/mathutil.h" @@ -234,11 +235,10 @@ DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCost( DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCostWithoutFixedTransits( int vehicle, const std::function& next_accessor, + const RoutingModel::ResourceGroup::Resource* resource, int64_t* optimal_cost_without_transits) { return optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, - /*dimension_travel_info=*/{}, - /*resource=*/nullptr, + vehicle, next_accessor, /*dimension_travel_info=*/{}, resource, /*optimize_vehicle_costs=*/optimal_cost_without_transits != nullptr, solver_[vehicle].get(), /*cumul_values=*/nullptr, /*break_values=*/nullptr, optimal_cost_without_transits, nullptr); @@ -268,7 +268,8 @@ DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumuls( return optimizer_core_.OptimizeSingleRouteWithResource( vehicle, next_accessor, dimension_travel_info, resource, /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), optimal_cumuls, - optimal_breaks, /*cost=*/nullptr, /*transit_cost=*/nullptr); + optimal_breaks, /*cost_without_transit=*/nullptr, + /*transit_cost=*/nullptr); } DimensionSchedulingStatus @@ -570,7 +571,9 @@ DimensionCumulOptimizerCore::DimensionCumulOptimizerCore( all_break_variables_.resize(num_break_vars, -1); } if (!model.GetDimensionResourceGroupIndices(dimension_).empty()) { - resource_group_to_resource_to_vehicle_assignment_variables_.resize( + resource_class_to_vehicle_assignment_variables_per_group_.resize( + model.GetResourceGroups().size()); + resource_class_ignored_resources_per_group_.resize( model.GetResourceGroups().size()); } } @@ -755,12 +758,16 @@ bool GetIntervalIntersectionWithOffsetDomain(const ClosedInterval& interval, const Domain& domain, int64_t offset, ClosedInterval* intersection) { - ClosedInterval domain_bounds; - if (!GetDomainOffsetBounds(domain, offset, &domain_bounds)) { + if (domain == Domain::AllValues()) { + *intersection = interval; + return true; + } + ClosedInterval offset_domain; + if (!GetDomainOffsetBounds(domain, offset, &offset_domain)) { return false; } - const int64_t intersection_lb = std::max(interval.start, domain_bounds.start); - const int64_t intersection_ub = std::min(interval.end, domain_bounds.end); + const int64_t intersection_lb = std::max(interval.start, offset_domain.start); + const int64_t intersection_ub = std::min(interval.end, offset_domain.end); if (intersection_lb > intersection_ub) return false; *intersection = ClosedInterval(intersection_lb, intersection_ub); @@ -825,8 +832,8 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( int vehicle, const std::function& next_accessor, const std::function& transit_accessor, const RouteDimensionTravelInfo& dimension_travel_info, - const std::vector& resources, - const std::vector& resource_indices, bool optimize_vehicle_costs, + absl::Span resources, + absl::Span resource_indices, bool optimize_vehicle_costs, RoutingLinearSolverWrapper* solver, std::vector>* cumul_values, std::vector>* break_values, @@ -1088,7 +1095,8 @@ DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( vehicle, next_accessor, dimension_travel_info, resource, /*optimize_vehicle_costs=*/true, solver, /*cumul_values=*/nullptr, /*break_values=*/nullptr, - /*cost=*/nullptr, /*transit_cost=*/nullptr, /*clear_lp=*/false); + /*cost_without_transit=*/nullptr, /*transit_cost=*/nullptr, + /*clear_lp=*/false); if (status != DimensionSchedulingStatus::INFEASIBLE) { status = PackRoutes({vehicle}, solver, packing_parameters); @@ -1213,7 +1221,7 @@ bool DimensionCumulOptimizerCore::ExtractRouteCumulBounds( } bool DimensionCumulOptimizerCore::TightenRouteCumulBounds( - const std::vector& route, const std::vector& min_transits, + absl::Span route, absl::Span min_transits, int64_t cumul_offset) { const int route_size = route.size(); if (propagator_ != nullptr) { @@ -1265,7 +1273,7 @@ bool DimensionCumulOptimizerCore::TightenRouteCumulBounds( } std::vector SlopeAndYInterceptToConvexityRegions( - const std::vector& slope_and_y_intercept) { + absl::Span slope_and_y_intercept) { CHECK(!slope_and_y_intercept.empty()); std::vector convex(slope_and_y_intercept.size(), false); double previous_slope = std::numeric_limits::max(); @@ -1390,8 +1398,7 @@ int64_t ComputeConvexPiecewiseLinearFormulationValue( bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( const RouteDimensionTravelInfo& dimension_travel_info, - const std::vector& lp_slacks, - const std::vector& fixed_transit, + absl::Span lp_slacks, absl::Span fixed_transit, RoutingLinearSolverWrapper* solver) { const std::vector& lp_cumuls = current_route_cumul_variables_; const int path_size = lp_cumuls.size(); @@ -2318,6 +2325,16 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( return true; } +namespace { +bool AllValuesContainedExcept(const IntVar& var, absl::Span values, + const absl::flat_hash_set& ignored_values) { + for (int val : values) { + if (!ignored_values.contains(val) && !var.Contains(val)) return false; + } + return true; +} +} // namespace + bool DimensionCumulOptimizerCore::SetGlobalConstraints( const std::function& next_accessor, int64_t cumul_offset, bool optimize_costs, bool optimize_resource_assignment, @@ -2378,45 +2395,78 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment( return true; } + using RCIndex = RoutingModel::ResourceClassIndex; const RoutingModel& model = *dimension_->model(); const int num_vehicles = model.vehicles(); const auto& resource_groups = model.GetResourceGroups(); for (int rg_index : model.GetDimensionResourceGroupIndices(dimension_)) { // Resource domain constraints: // Every (used) vehicle requiring a resource from this group must be - // assigned to exactly one resource in this group, and each resource must - // be assigned to at most 1 vehicle requiring it. For every resource r - // with Attributes A = resources[r].attributes(dim) and every vehicle v, - // assign(r, v) == 1 --> A.start_domain.Min() <= cumul[Start(v)] <= - // A.start_domain.Max(), and A.end_domain.Min() <= cumul[End(v)] <= - // A.end_domain.Max(). + // assigned to exactly one resource-class in this group, and each + // resource-class must be assigned to at most #available_resources_in_class + // vehicles requiring a resource from the group. + // For every resource-class rc having a resource r with Attributes + // A = resources[r].attributes(dim) and every vehicle v, + // assign(rc, v) == 1 --> + // A.start_domain.Min() <= cumul[Start(v)] <= A.start_domain.Max() + // and + // A.end_domain.Min() <= cumul[End(v)] <= A.end_domain.Max() const ResourceGroup& resource_group = *resource_groups[rg_index]; DCHECK(!resource_group.GetVehiclesRequiringAResource().empty()); - const std::vector& resources = - resource_group.GetResources(); + static const int kNoConstraint = RoutingLinearSolverWrapper::kNoConstraint; int num_required_resources = 0; - static const int kNoConstraint = -1; // Assignment constraints for vehicles: each (used) vehicle must have // exactly one resource assigned to it. std::vector vehicle_constraints(model.vehicles(), kNoConstraint); + const int num_resource_classes = resource_group.GetResourceClassesCount(); + std::vector>& ignored_resources_per_class = + resource_class_ignored_resources_per_group_[rg_index]; + ignored_resources_per_class.assign(num_resource_classes, {}); for (int v : resource_group.GetVehiclesRequiringAResource()) { + const IntVar& resource_var = *model.ResourceVar(v, rg_index); if (model.IsEnd(next_accessor(model.Start(v))) && !model.IsVehicleUsedWhenEmpty(v)) { - // We don't assign a driver to unused vehicles. + if (resource_var.Bound() && resource_var.Value() >= 0) { + // Resource var forces this vehicle to be used. + return false; + } + // We don't assign a resource to unused vehicles. + continue; + } + // Vehicle is used. + if (resource_var.Bound()) { + const int resource_index = resource_var.Value(); + if (resource_index < 0) { + // This vehicle is used but has a negative resource enforced. + return false; + } + ignored_resources_per_class + [resource_group.GetResourceClassIndex(resource_index).value()] + .insert(resource_index); + // We add the corresponding domain constraint on the vehicle. + const int start_index = index_to_cumul_variable_[model.Start(v)]; + const int end_index = index_to_cumul_variable_[model.End(v)]; + if (!TightenStartEndVariableBoundsWithResource( + *dimension_, resource_group.GetResource(resource_index), + GetVariableBounds(start_index, *solver), start_index, + GetVariableBounds(end_index, *solver), end_index, cumul_offset, + solver)) { + return false; + } continue; } num_required_resources++; vehicle_constraints[v] = solver->CreateNewConstraint(1, 1); } - // Assignment constraints for resources: each resource must be assigned to - // at most one (used) vehicle requiring one. - const int num_resources = resources.size(); - std::vector resource_constraints(num_resources, kNoConstraint); + // Assignment constraints for resource classes: each resource-class must be + // assigned to at most #available_resources_in_class (used) vehicles + // requiring it. + std::vector resource_class_cstrs(num_resource_classes, kNoConstraint); int num_available_resources = 0; - for (int r = 0; r < num_resources; r++) { + for (RCIndex rc_index(0); rc_index < num_resource_classes; rc_index++) { const ResourceGroup::Attributes& attributes = - resources[r].GetDimensionAttributes(dimension_); + resource_group.GetDimensionAttributesForClass(dimension_, rc_index); if (attributes.start_domain().Max() < cumul_offset || attributes.end_domain().Max() < cumul_offset) { // This resource's domain has a cumul max lower than the offset, so @@ -2424,8 +2474,16 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment( // skip it. continue; } - num_available_resources++; - resource_constraints[r] = solver->CreateNewConstraint(0, 1); + const int rc = rc_index.value(); + const int num_available_class_resources = + resource_group.GetResourceIndicesInClass(rc_index).size() - + ignored_resources_per_class[rc].size(); + DCHECK_GE(num_available_class_resources, 0); + if (num_available_class_resources > 0) { + num_available_resources += num_available_class_resources; + resource_class_cstrs[rc] = + solver->CreateNewConstraint(0, num_available_class_resources); + } } if (num_required_resources > num_available_resources) { @@ -2434,12 +2492,12 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment( return false; } - std::vector& resource_to_vehicle_assignment_variables = - resource_group_to_resource_to_vehicle_assignment_variables_[rg_index]; - resource_to_vehicle_assignment_variables.assign( - num_resources * num_vehicles, -1); + std::vector& resource_class_to_vehicle_assignment_vars = + resource_class_to_vehicle_assignment_variables_per_group_[rg_index]; + resource_class_to_vehicle_assignment_vars.assign( + num_resource_classes * num_vehicles, -1); // Create assignment variables, add them to the corresponding constraints, - // and create the reified constraints assign(r, v) == 1 --> + // and create the reified constraints assign(rc, v) == 1 --> // A(r).start_domain.Min() <= cumul[Start(v)] <= A(r).start_domain.Max(), // and // A(r).end_domain.Min() <= cumul[End(v)] <= A(r).end_domain.Max(). @@ -2449,30 +2507,43 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment( IntVar* const resource_var = model.ResourceVar(v, rg_index); std::unique_ptr it( resource_var->MakeDomainIterator(false)); + std::vector resource_class_considered(num_resource_classes, false); for (int r : InitAndGetValues(it.get())) { if (r < 0) continue; - DCHECK_LT(r, num_resources); - if (resource_constraints[r] == kNoConstraint) continue; - - const int assign_r_to_v = solver->AddVariable(0, 1); - SET_DEBUG_VARIABLE_NAME( - solver, assign_r_to_v, - absl::StrFormat("assign_r_to_v(%ld, %ld)", r, v)); - resource_to_vehicle_assignment_variables[r * num_vehicles + v] = - assign_r_to_v; - // To keep the model clean - // (cf. glop::LinearProgram::NotifyThatColumnsAreClean), constraints - // on assign_r_to_v need to be in ascending order. - if (vehicle_constraints[v] < resource_constraints[r]) { - solver->SetCoefficient(vehicle_constraints[v], assign_r_to_v, 1); - solver->SetCoefficient(resource_constraints[r], assign_r_to_v, 1); - } else { - solver->SetCoefficient(resource_constraints[r], assign_r_to_v, 1); - solver->SetCoefficient(vehicle_constraints[v], assign_r_to_v, 1); + const RCIndex rc_index = resource_group.GetResourceClassIndex(r); + const int rc = rc_index.value(); + if (resource_class_considered[rc]) { + continue; } + resource_class_considered[rc] = true; + if (resource_class_cstrs[rc] == kNoConstraint) continue; + + // NOTE(user): The resource class computation should allow us to + // catch all incompatibility reasons between vehicles and resources. If + // the following DCHECK fails, the resource classes should be adapted + // accordingly. + DCHECK(AllValuesContainedExcept( + *resource_var, resource_group.GetResourceIndicesInClass(rc_index), + ignored_resources_per_class[rc])) + << DUMP_VARS(v, rg_index, + resource_group.GetResourceIndicesInClass(rc_index), + resource_var->Min(), resource_var->Max()); + + const int assign_rc_to_v = solver->AddVariable(0, 1); + SET_DEBUG_VARIABLE_NAME( + solver, assign_rc_to_v, + absl::StrFormat("assign_rc_to_v(%ld, %ld)", rc, v)); + resource_class_to_vehicle_assignment_vars[rc * num_vehicles + v] = + assign_rc_to_v; + // To keep the model clean + // (cf. glop::LinearProgram::NotifyThatColumnsAreClean), constraints on + // assign_rc_to_v need to be in ascending order. + DCHECK_LT(vehicle_constraints[v], resource_class_cstrs[rc]); + solver->SetCoefficient(vehicle_constraints[v], assign_rc_to_v, 1); + solver->SetCoefficient(resource_class_cstrs[rc], assign_rc_to_v, 1); const auto& add_domain_constraint = - [&solver, cumul_offset, assign_r_to_v](const Domain& domain, + [&solver, cumul_offset, assign_rc_to_v](const Domain& domain, int cumul_variable) { if (domain == Domain::AllValues()) { return; @@ -2480,15 +2551,15 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment( ClosedInterval cumul_bounds; if (!GetDomainOffsetBounds(domain, cumul_offset, &cumul_bounds)) { // This domain cannot be assigned to this vehicle. - solver->SetVariableBounds(assign_r_to_v, 0, 0); + solver->SetVariableBounds(assign_rc_to_v, 0, 0); return; } const int cumul_constraint = solver->AddLinearConstraint( cumul_bounds.start, cumul_bounds.end, {{cumul_variable, 1}}); - solver->SetEnforcementLiteral(cumul_constraint, assign_r_to_v); + solver->SetEnforcementLiteral(cumul_constraint, assign_rc_to_v); }; const ResourceGroup::Attributes& attributes = - resources[r].GetDimensionAttributes(dimension_); + resource_group.GetDimensionAttributesForClass(dimension_, rc_index); add_domain_constraint(attributes.start_domain(), index_to_cumul_variable_[model.Start(v)]); add_domain_constraint(attributes.end_domain(), @@ -2502,7 +2573,7 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment( #undef SET_DEBUG_VARIABLE_NAME void DimensionCumulOptimizerCore::SetValuesFromLP( - const std::vector& lp_variables, int64_t offset, + absl::Span lp_variables, int64_t offset, RoutingLinearSolverWrapper* solver, std::vector* lp_values) const { if (lp_values == nullptr) return; lp_values->assign(lp_variables.size(), std::numeric_limits::min()); @@ -2522,9 +2593,10 @@ void DimensionCumulOptimizerCore::SetResourceIndices( RoutingLinearSolverWrapper* solver, std::vector>* resource_indices_per_group) const { if (resource_indices_per_group == nullptr || - resource_group_to_resource_to_vehicle_assignment_variables_.empty()) { + resource_class_to_vehicle_assignment_variables_per_group_.empty()) { return; } + using RCIndex = RoutingModel::ResourceClassIndex; const RoutingModel& model = *dimension_->model(); const int num_vehicles = model.vehicles(); DCHECK(!model.GetDimensionResourceGroupIndices(dimension_).empty()); @@ -2534,22 +2606,42 @@ void DimensionCumulOptimizerCore::SetResourceIndices( const ResourceGroup& resource_group = *resource_groups[rg_index]; DCHECK(!resource_group.GetVehiclesRequiringAResource().empty()); - const int num_resources = resource_group.Size(); + const absl::StrongVector>& + resource_indices_per_class = + resource_group.GetResourceIndicesPerClass(); + const int num_resource_classes = resource_group.GetResourceClassesCount(); + std::vector current_resource_pos_for_class(num_resource_classes, 0); std::vector& resource_indices = resource_indices_per_group->at(rg_index); resource_indices.assign(num_vehicles, -1); // Find the resource assigned to each vehicle. - const std::vector& resource_to_vehicle_assignment_variables = - resource_group_to_resource_to_vehicle_assignment_variables_[rg_index]; - DCHECK_EQ(resource_to_vehicle_assignment_variables.size(), - num_resources * num_vehicles); + const std::vector& resource_class_to_vehicle_assignment_vars = + resource_class_to_vehicle_assignment_variables_per_group_[rg_index]; + DCHECK_EQ(resource_class_to_vehicle_assignment_vars.size(), + num_resource_classes * num_vehicles); + const std::vector>& ignored_resources_per_class = + resource_class_ignored_resources_per_group_[rg_index]; + DCHECK_EQ(ignored_resources_per_class.size(), num_resource_classes); for (int v : resource_group.GetVehiclesRequiringAResource()) { - for (int r = 0; r < num_resources; r++) { + const IntVar& resource_var = *model.ResourceVar(v, rg_index); + if (resource_var.Bound()) { + resource_indices[v] = resource_var.Value(); + continue; + } + for (int rc = 0; rc < num_resource_classes; rc++) { const int assignment_var = - resource_to_vehicle_assignment_variables[r * num_vehicles + v]; + resource_class_to_vehicle_assignment_vars[rc * num_vehicles + v]; if (assignment_var >= 0 && solver->GetValue(assignment_var) == 1) { - // This resource is assigned to this vehicle. - resource_indices[v] = r; + // This resource class is assigned to this vehicle. + const std::vector& class_resource_indices = + resource_indices_per_class[RCIndex(rc)]; + int& pos = current_resource_pos_for_class[rc]; + while (ignored_resources_per_class[rc].contains( + class_resource_indices[pos])) { + pos++; + DCHECK_LT(pos, class_resource_indices.size()); + } + resource_indices[v] = class_resource_indices[pos++]; break; } } @@ -2616,17 +2708,9 @@ DimensionSchedulingStatus GlobalDimensionCumulOptimizer::ComputePackedCumuls( namespace { -bool AllValuesContainedExcept(const IntVar& var, const std::vector& values, - const absl::flat_hash_set& ignored_values) { - for (int val : values) { - if (!ignored_values.contains(val) && !var.Contains(val)) return false; - } - return true; -} - template void MoveValuesToIndicesFrom(std::vector* out_values, - const std::vector& out_indices_to_evaluate, + absl::Span out_indices_to_evaluate, const std::function& index_evaluator, std::vector& values_to_copy) { if (out_values == nullptr) { @@ -2801,7 +2885,7 @@ int64_t ComputeBestVehicleToResourceAssignment( std::vector* resource_indices) { const int total_num_resources = absl::c_accumulate( resource_indices_per_class, 0, - [](int acc, const std::vector& res) { return acc + res.size(); }); + [](int acc, absl::Span res) { return acc + res.size(); }); DCHECK_GE(total_num_resources, 1); const int num_ignored_resources = absl::c_accumulate(ignored_resources_per_class, 0, diff --git a/ortools/constraint_solver/routing_lp_scheduling.h b/ortools/constraint_solver/routing_lp_scheduling.h index d4308dc454..b93e124055 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.h +++ b/ortools/constraint_solver/routing_lp_scheduling.h @@ -166,6 +166,8 @@ enum class DimensionSchedulingStatus { class RoutingLinearSolverWrapper { public: + static const int kNoConstraint = -1; + virtual ~RoutingLinearSolverWrapper() {} virtual void Clear() = 0; virtual int CreateNewPositiveVariable() = 0; @@ -674,8 +676,8 @@ class DimensionCumulOptimizerCore { int vehicle, const std::function& next_accessor, const std::function& transit_accessor, const RouteDimensionTravelInfo& dimension_travel_info, - const std::vector& resources, - const std::vector& resource_indices, bool optimize_vehicle_costs, + absl::Span resources, + absl::Span resource_indices, bool optimize_vehicle_costs, RoutingLinearSolverWrapper* solver, std::vector>* cumul_values, std::vector>* break_values, @@ -725,8 +727,8 @@ class DimensionCumulOptimizerCore { // Tighten the minimum/maximum of cumuls for nodes on "route" // If the propagator_ is not null, uses the bounds tightened by the // propagator. Otherwise, the minimum transits are used to tighten them. - bool TightenRouteCumulBounds(const std::vector& route, - const std::vector& min_transits, + bool TightenRouteCumulBounds(absl::Span route, + absl::Span min_transits, int64_t cumul_offset); // Sets the constraints for all nodes on "vehicle"'s route according to @@ -746,8 +748,7 @@ class DimensionCumulOptimizerCore { // Returns false if some infeasibility was detected, true otherwise. bool SetRouteTravelConstraints( const RouteDimensionTravelInfo& dimension_travel_info, - const std::vector& lp_slacks, - const std::vector& fixed_transit, + absl::Span lp_slacks, absl::Span fixed_transit, RoutingLinearSolverWrapper* solver); // Sets the global constraints on the dimension, and adds global objective @@ -766,7 +767,7 @@ class DimensionCumulOptimizerCore { const std::function& next_accessor, int64_t cumul_offset, RoutingLinearSolverWrapper* solver); - void SetValuesFromLP(const std::vector& lp_variables, int64_t offset, + void SetValuesFromLP(absl::Span lp_variables, int64_t offset, RoutingLinearSolverWrapper* solver, std::vector* lp_values) const; @@ -804,12 +805,16 @@ class DimensionCumulOptimizerCore { // all_break_variables_[vehicle_to_all_break_variables_offset_[vehicle]] to // all_break_variables[vehicle_to_all_break_variables_offset_[vehicle+1]-1]. std::vector vehicle_to_all_break_variables_offset_; - // The following vector contains indices of resource-to-vehicle assignment - // variables. For every resource group, stores indices of - // num_resources*num_vehicles boolean variables indicating whether resource #r - // is assigned to vehicle #v. + // The following vector contains indices of resource-class-to-vehicle + // assignment variables. For every resource group, stores indices of + // num_resource_classes*num_vehicles boolean variables indicating whether + // resource class #rc is assigned to vehicle #v. std::vector> - resource_group_to_resource_to_vehicle_assignment_variables_; + resource_class_to_vehicle_assignment_variables_per_group_; + // The following vector keeps track of the resources ignored during resource + // assignment because they're pre-assigned to a specific vehicle. + std::vector>> + resource_class_ignored_resources_per_group_; int max_end_cumul_; int min_start_cumul_; @@ -840,6 +845,7 @@ class LocalDimensionCumulOptimizer { // the part of the vehicle span cost due to fixed transits. DimensionSchedulingStatus ComputeRouteCumulCostWithoutFixedTransits( int vehicle, const std::function& next_accessor, + const RoutingModel::ResourceGroup::Resource* resource, int64_t* optimal_cost_without_transits); std::vector @@ -1048,7 +1054,7 @@ struct SlopeAndYIntercept { // function would yield {true, false, false, ...} and a concave function would // yield {true, true, true, ...}. std::vector SlopeAndYInterceptToConvexityRegions( - const std::vector& slope_and_y_intercept); + absl::Span slope_and_y_intercept); // Given a PiecewiseLinearFormulation, returns a vector of slope and y-intercept // corresponding to each segment. Only the segments in [index_start, index_end[ diff --git a/ortools/constraint_solver/routing_neighborhoods.cc b/ortools/constraint_solver/routing_neighborhoods.cc index a92f0be6dc..882f218d97 100644 --- a/ortools/constraint_solver/routing_neighborhoods.cc +++ b/ortools/constraint_solver/routing_neighborhoods.cc @@ -310,7 +310,7 @@ int MakePairActiveOperator::FindNextInactivePair(int pair_index) const { } bool MakePairActiveOperator::ContainsActiveNodes( - const std::vector& nodes) const { + absl::Span nodes) const { for (int64_t node : nodes) { if (!IsInactive(node)) return true; } @@ -1044,7 +1044,7 @@ RelocateSubtrip::RelocateSubtrip( const std::vector& secondary_vars, std::function start_empty_path_class, std::function&(int, int)> get_neighbors, - const std::vector& pairs) + absl::Span pairs) : PathOperator(vars, secondary_vars, /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, /*skip_locally_optimal_paths=*/true, @@ -1172,7 +1172,7 @@ ExchangeSubtrip::ExchangeSubtrip( const std::vector& secondary_vars, std::function start_empty_path_class, std::function&(int, int)> get_neighbors, - const std::vector& pairs) + absl::Span pairs) : PathOperator(vars, secondary_vars, /*number_of_base_nodes=*/get_neighbors == nullptr ? 2 : 1, /*skip_locally_optimal_paths=*/true, @@ -1182,7 +1182,7 @@ ExchangeSubtrip::ExchangeSubtrip( opened_pairs_set_.resize(pairs.size(), false); } -void ExchangeSubtrip::SetPath(const std::vector& path, int path_id) { +void ExchangeSubtrip::SetPath(absl::Span path, int path_id) { for (int i = 1; i < path.size(); ++i) { SetNext(path[i - 1], path[i], path_id); } diff --git a/ortools/constraint_solver/routing_neighborhoods.h b/ortools/constraint_solver/routing_neighborhoods.h index 9c7f0ffae4..d2b46ee77b 100644 --- a/ortools/constraint_solver/routing_neighborhoods.h +++ b/ortools/constraint_solver/routing_neighborhoods.h @@ -187,7 +187,7 @@ class MakePairActiveOperator : public PathOperator { private: void OnNodeInitialization() override; int FindNextInactivePair(int pair_index) const; - bool ContainsActiveNodes(const std::vector& nodes) const; + bool ContainsActiveNodes(absl::Span nodes) const; int inactive_pair_; int inactive_pair_first_index_; @@ -659,11 +659,11 @@ class RelocateSubtrip : public PathOperator { const std::vector& secondary_vars, std::function start_empty_path_class, std::function&(int, int)> get_neighbors, - const std::vector& pairs); + absl::Span pairs); RelocateSubtrip(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, - const std::vector& pairs) + absl::Span pairs) : RelocateSubtrip(vars, secondary_vars, std::move(start_empty_path_class), nullptr, pairs) {} @@ -696,7 +696,7 @@ class ExchangeSubtrip : public PathOperator { const std::vector& secondary_vars, std::function start_empty_path_class, std::function&(int, int)> get_neighbors, - const std::vector& pairs); + absl::Span pairs); ExchangeSubtrip(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, @@ -734,7 +734,7 @@ class ExchangeSubtrip : public PathOperator { bool ExtractChainsFromDelivery(int64_t base_node, std::vector* rejects, std::vector* subtrip); - void SetPath(const std::vector& path, int path_id); + void SetPath(absl::Span path, int path_id); const PickupAndDeliveryData pd_data_; // Represents the set of opened pairs during ExtractChainsFromXXX(). diff --git a/ortools/constraint_solver/routing_parameters.cc b/ortools/constraint_solver/routing_parameters.cc index 3af9ef3751..eafdcbf625 100644 --- a/ortools/constraint_solver/routing_parameters.cc +++ b/ortools/constraint_solver/routing_parameters.cc @@ -60,6 +60,11 @@ IteratedLocalSearchParameters CreateDefaultIteratedLocalSearchParameters() { rr->set_num_ruined_routes(2); ils.set_improve_perturbed_solution(true); ils.set_acceptance_strategy(AcceptanceStrategy::GREEDY_DESCENT); + SimulatedAnnealingParameters* sa = + ils.mutable_simulated_annealing_parameters(); + sa->set_cooling_schedule_strategy(CoolingScheduleStrategy::EXPONENTIAL); + sa->set_initial_temperature(100.0); + sa->set_final_temperature(0.01); return ils; } @@ -71,7 +76,6 @@ RoutingSearchParameters CreateDefaultRoutingSearchParameters() { p.set_savings_max_memory_usage_bytes(6e9); p.set_savings_add_reverse_arcs(false); p.set_savings_arc_coefficient(1); - p.set_savings_parallel_routes(false); p.set_cheapest_insertion_farthest_seeds_ratio(0); p.set_cheapest_insertion_first_solution_neighbors_ratio(1); p.set_cheapest_insertion_first_solution_min_neighbors(1); @@ -229,6 +233,127 @@ bool IsValidNonNegativeDuration(const google::protobuf::Duration& d) { return status_or_duration.ok() && status_or_duration.value() >= absl::ZeroDuration(); } + +// Searches for errors in ILS parameters and appends them to the given `errors` +// vector. +void FindErrorsInIteratedLocalSearchParameters( + const RoutingSearchParameters& search_parameters, + std::vector& errors) { + using absl::StrCat; + if (!search_parameters.use_iterated_local_search()) { + return; + } + + if (!search_parameters.has_iterated_local_search_parameters()) { + errors.emplace_back( + "use_iterated_local_search is true but " + "iterated_local_search_parameters are missing."); + return; + } + + const IteratedLocalSearchParameters& ils = + search_parameters.iterated_local_search_parameters(); + + if (ils.perturbation_strategy() == PerturbationStrategy::UNSET) { + errors.emplace_back( + StrCat("Invalid value for " + "iterated_local_search_parameters.perturbation_strategy: ", + ils.perturbation_strategy())); + } + + if (ils.perturbation_strategy() == PerturbationStrategy::RUIN_AND_RECREATE) { + if (!ils.has_ruin_recreate_parameters()) { + errors.emplace_back(StrCat( + "iterated_local_search_parameters.perturbation_strategy is ", + PerturbationStrategy::RUIN_AND_RECREATE, + " but iterated_local_search_parameters.ruin_recreate_parameters are " + "missing.")); + return; + } + + const RuinRecreateParameters& rr = ils.ruin_recreate_parameters(); + + if (rr.ruin_strategy() == RuinStrategy::UNSET) { + errors.emplace_back( + StrCat("Invalid value for " + "iterated_local_search_parameters.ruin_recreate_parameters." + "ruin_strategy: ", + rr.ruin_strategy())); + } + + if (rr.ruin_strategy() == RuinStrategy::SPATIALLY_CLOSE_ROUTES_REMOVAL && + rr.num_ruined_routes() <= 0) { + errors.emplace_back( + StrCat("iterated_local_search_parameters.ruin_recreate_parameters." + "ruin_strategy is set to ", + rr.ruin_strategy(), + " but " + "iterated_local_search_parameters.ruin_recreate_parameters." + "num_ruined_routes is ", + rr.num_ruined_routes())); + } + + if (rr.recreate_strategy() == FirstSolutionStrategy::UNSET) { + errors.emplace_back( + StrCat("Invalid value for " + "iterated_local_search_parameters.ruin_recreate_parameters." + "recreate_strategy: ", + rr.recreate_strategy())); + } + } + + if (ils.acceptance_strategy() == AcceptanceStrategy::UNSET) { + errors.emplace_back( + StrCat("Invalid value for " + "iterated_local_search_parameters.acceptance_strategy: ", + ils.acceptance_strategy())); + } + + if (ils.acceptance_strategy() == AcceptanceStrategy::SIMULATED_ANNEALING) { + if (!ils.has_simulated_annealing_parameters()) { + errors.emplace_back( + StrCat("iterated_local_search_parameters.acceptance_strategy is ", + AcceptanceStrategy::SIMULATED_ANNEALING, + " but " + "iterated_local_search_parameters.simulated_annealing_" + "parameters are missing.")); + return; + } + + const SimulatedAnnealingParameters& sa_params = + ils.simulated_annealing_parameters(); + + if (sa_params.cooling_schedule_strategy() == + CoolingScheduleStrategy::UNSET) { + errors.emplace_back( + StrCat("Invalid value for " + "iterated_local_search_parameters.simulated_annealing_" + "parameters.cooling_schedule_strategy: ", + sa_params.cooling_schedule_strategy())); + } + + if (sa_params.initial_temperature() < sa_params.final_temperature()) { + errors.emplace_back( + "iterated_local_search_parameters.simulated_annealing_parameters." + "initial_temperature cannot be lower than " + "iterated_local_search_parameters.simulated_annealing_parameters." + "final_temperature."); + } + + if (sa_params.initial_temperature() < 1e-9) { + errors.emplace_back( + "iterated_local_search_parameters.simulated_annealing_parameters." + "initial_temperature cannot be lower than 1e-9."); + } + + if (sa_params.final_temperature() < 1e-9) { + errors.emplace_back( + "iterated_local_search_parameters.simulated_annealing_parameters." + "final_temperature cannot be lower than 1e-9."); + } + } +} + } // namespace std::string FindErrorInRoutingSearchParameters( @@ -485,6 +610,8 @@ std::vector FindErrorsInRoutingSearchParameters( " search"); } + FindErrorsInIteratedLocalSearchParameters(search_parameters, errors); + return errors; } diff --git a/ortools/constraint_solver/routing_parameters.proto b/ortools/constraint_solver/routing_parameters.proto index ce3970424b..e549f79285 100644 --- a/ortools/constraint_solver/routing_parameters.proto +++ b/ortools/constraint_solver/routing_parameters.proto @@ -38,6 +38,8 @@ package operations_research; // To see those "default" parameters, call GetDefaultRoutingSearchParameters(). // Next ID: 61 message RoutingSearchParameters { + reserved 19; + // First solution strategies, used as starting point of local search. FirstSolutionStrategy.Value first_solution_strategy = 1; @@ -66,8 +68,6 @@ message RoutingSearchParameters { // - savings_arc_coefficient * Cost(a-->b) // This parameter must be greater than 0, and its default value is 1. double savings_arc_coefficient = 18; - // When true, the routes are built in parallel, sequentially otherwise. - bool savings_parallel_routes = 19; // Ratio (between 0 and 1) of available vehicles in the model on which // farthest nodes of the model are inserted as seeds in the diff --git a/ortools/constraint_solver/routing_sat.cc b/ortools/constraint_solver/routing_sat.cc index 875c3663ae..3e9508ec92 100644 --- a/ortools/constraint_solver/routing_sat.cc +++ b/ortools/constraint_solver/routing_sat.cc @@ -584,7 +584,7 @@ void AddGeneralizedDimensions( std::vector CreateGeneralizedRanks(const RoutingModel& model, const ArcVarMap& arc_vars, - const std::vector& is_unperformed, + absl::Span is_unperformed, CpModelProto* cp_model) { const int depot = 0; const int num_cp_nodes = model.Nexts().size() + model.vehicles() + 1; @@ -612,7 +612,7 @@ std::vector CreateGeneralizedRanks(const RoutingModel& model, void AddGeneralizedPickupDeliveryConstraints( const RoutingModel& model, const ArcVarMap& arc_vars, - const std::vector>& vehicle_performs_node, + absl::Span> vehicle_performs_node, const std::vector& is_unperformed, CpModelProto* cp_model) { if (model.GetPickupAndDeliveryPairs().empty()) return; const std::vector ranks = diff --git a/ortools/constraint_solver/routing_search.cc b/ortools/constraint_solver/routing_search.cc index 39774eb14b..497b137a0a 100644 --- a/ortools/constraint_solver/routing_search.cc +++ b/ortools/constraint_solver/routing_search.cc @@ -297,9 +297,9 @@ void IntVarFilteredHeuristic::ResetSolution() { number_of_rejects_ = 0; // Wiping assignment when starting a new search. assignment_->MutableIntVarContainer()->Clear(); - assignment_->MutableIntVarContainer()->Resize(vars_.size()); delta_->MutableIntVarContainer()->Clear(); SynchronizeFilters(); + assignment_->MutableIntVarContainer()->Resize(vars_.size()); } Assignment* IntVarFilteredHeuristic::BuildSolution() { @@ -2530,7 +2530,7 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPairMultitour( : Value(insertion.pred); const int64_t cost = GetInsertionCostForNodeAtPosition( insertion.node, insertion.pred, succ, sequence.Vehicle()); - sequence_cost = CapAdd(sequence_cost, cost); + CapAddTo(cost, &sequence_cost); previous_node = insertion.node; previous_succ = succ; } @@ -2544,7 +2544,7 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPairMultitour( } const int64_t new_cost = bin_capacities->TotalCost(); const int64_t delta_cost = CapSub(new_cost, old_cost); - sequence.Cost() = CapAdd(sequence.Cost(), delta_cost); + CapAddTo(delta_cost, &sequence.Cost()); for (const Insertion& insertion : sequence) { bin_capacities->RemoveItemFromBin(insertion.node, sequence.Vehicle()); @@ -2762,8 +2762,7 @@ LocalCheapestInsertionFilteredHeuristic::ComputeEvaluatorSortedPositions( const int64_t delta_cost = CapSub(new_cost, old_cost); // Add soft cost to new insertions. for (size_t i = old_num_insertions; i < sorted_insertions.size(); ++i) { - sorted_insertions[i].value = - CapAdd(sorted_insertions[i].value, delta_cost); + CapAddTo(delta_cost, &sorted_insertions[i].value); } } } @@ -2836,7 +2835,7 @@ LocalCheapestInsertionFilteredHeuristic::ComputeEvaluatorSortedPairPositions( bin_capacities_->AddItemToBin(pickup, vehicle); bin_capacities_->AddItemToBin(delivery, vehicle); const int64_t new_cost = bin_capacities_->TotalCost(); - total_cost = CapAdd(total_cost, CapSub(new_cost, old_cost)); + CapAddTo(CapSub(new_cost, old_cost), &total_cost); bin_capacities_->RemoveItemFromBin(pickup, vehicle); bin_capacities_->RemoveItemFromBin(delivery, vehicle); } diff --git a/ortools/constraint_solver/routing_utils.cc b/ortools/constraint_solver/routing_utils.cc index bd2de57aaf..192602f10e 100644 --- a/ortools/constraint_solver/routing_utils.cc +++ b/ortools/constraint_solver/routing_utils.cc @@ -21,6 +21,7 @@ #include #include "absl/log/check.h" +#include "absl/types/span.h" #include "ortools/util/saturated_arithmetic.h" namespace operations_research { @@ -45,7 +46,7 @@ bool BinCapacities::CheckAdditionFeasibility(int item, int bin) const { return CheckAdditionsFeasibility({item}, bin); } -bool BinCapacities::CheckAdditionsFeasibility(const std::vector& items, +bool BinCapacities::CheckAdditionsFeasibility(absl::Span items, int bin) const { for (size_t dim = 0; dim < load_demands_per_dimension_.size(); ++dim) { const LoadLimit& limit = load_limits_per_bin_[bin][dim]; diff --git a/ortools/constraint_solver/routing_utils.h b/ortools/constraint_solver/routing_utils.h index f6e0ba0e8d..2a82d3b221 100644 --- a/ortools/constraint_solver/routing_utils.h +++ b/ortools/constraint_solver/routing_utils.h @@ -19,6 +19,8 @@ #include #include +#include "absl/types/span.h" + namespace operations_research { // Tracks whether bins constrained by several nonnegative dimensions can contain @@ -51,7 +53,7 @@ class BinCapacities { // Checks whether adding item(s) is feasible w.r.t. dimensions. bool CheckAdditionFeasibility(int item, int bin) const; - bool CheckAdditionsFeasibility(const std::vector& items, int bin) const; + bool CheckAdditionsFeasibility(absl::Span items, int bin) const; // Adds item to bin, returns whether the bin is feasible. // The item is still added even when infeasible. bool AddItemToBin(int item, int bin); diff --git a/ortools/constraint_solver/samples/VrpInitialRoutes.cs b/ortools/constraint_solver/samples/VrpInitialRoutes.cs index 326dd9e852..fa3bda4277 100644 --- a/ortools/constraint_solver/samples/VrpInitialRoutes.cs +++ b/ortools/constraint_solver/samples/VrpInitialRoutes.cs @@ -149,8 +149,7 @@ public class InitialRoutes // Solve the problem. // [START solve] - Assignment solution = routing.SolveFromAssignmentWithParameters( - initialSolution, searchParameters); + Assignment solution = routing.SolveFromAssignmentWithParameters(initialSolution, searchParameters); // [END solve] // Print solution on console. diff --git a/ortools/constraint_solver/samples/VrpInitialRoutes.java b/ortools/constraint_solver/samples/VrpInitialRoutes.java index 0641e76706..6c75910221 100644 --- a/ortools/constraint_solver/samples/VrpInitialRoutes.java +++ b/ortools/constraint_solver/samples/VrpInitialRoutes.java @@ -145,8 +145,8 @@ public class VrpInitialRoutes { // Solve the problem. // [START solve] - Assignment solution = routing.solveFromAssignmentWithParameters( - initialSolution, searchParameters); + Assignment solution = + routing.solveFromAssignmentWithParameters(initialSolution, searchParameters); // [END solve] // Print solution on console. diff --git a/ortools/constraint_solver/samples/vrp_solution_callback.py b/ortools/constraint_solver/samples/vrp_solution_callback.py index b1b4d65f29..4effcee167 100755 --- a/ortools/constraint_solver/samples/vrp_solution_callback.py +++ b/ortools/constraint_solver/samples/vrp_solution_callback.py @@ -108,13 +108,17 @@ class SolutionCallback: self.objectives = [] def __call__(self): - objective = int(self._routing_model_ref().CostVar().Value()) + objective = int( + self._routing_model_ref().CostVar().Value() + ) # pytype: disable=attribute-error if not self.objectives or objective < self.objectives[-1]: self.objectives.append(objective) - print_solution(self._routing_manager_ref(), self._routing_model_ref()) + print_solution( + self._routing_manager_ref(), self._routing_model_ref() + ) # pytype: disable=attribute-error self._counter += 1 if self._counter > self._counter_limit: - self._routing_model_ref().solver().FinishCurrentSearch() + self._routing_model_ref().solver().FinishCurrentSearch() # pytype: disable=attribute-error # [END solution_callback] diff --git a/ortools/constraint_solver/search.cc b/ortools/constraint_solver/search.cc index 2f8560d775..fb40bdcde5 100644 --- a/ortools/constraint_solver/search.cc +++ b/ortools/constraint_solver/search.cc @@ -33,6 +33,7 @@ #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" +#include "absl/types/span.h" #include "ortools/base/bitmap.h" #include "ortools/base/logging.h" #include "ortools/base/mathutil.h" @@ -112,7 +113,7 @@ bool SearchLog::AtSolution() { std::string obj_str = ""; std::vector current; bool objective_updated = false; - const auto scaled_str = [this](const std::vector& values) { + const auto scaled_str = [this](absl::Span values) { std::vector value_strings(values.size()); for (int i = 0; i < values.size(); ++i) { if (scaling_factors_[i] != 1.0 || offsets_[i] != 0.0) { @@ -2983,6 +2984,7 @@ void ObjectiveMonitor::EnterSearch() { found_initial_solution_ = false; best_values_.assign(Size(), std::numeric_limits::max()); current_values_ = best_values_; + solver()->SetUseFastLocalSearch(true); } bool ObjectiveMonitor::AtSolution() {